chatflux-gestao-agente 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,15 +8,46 @@ faz na tela, e tudo no histórico com o seu nome.
8
8
  > nova, o que exige (e o que não exige) republicar, como testar sem browser, e as
9
9
  > armadilhas que já custaram tempo. Ele fica só no repo — não vai no npm.
10
10
 
11
+ ## Atualizar pelo portal
12
+
13
+ No aviso “Atualize o ChatFlux neste computador”, clique **Atualizar**. Com o
14
+ computador conectado e o programa 0.2.4 ou mais recente, o portal acompanha a
15
+ configuração completa, sem terminal. Se precisar renovar o login, o navegador
16
+ abre. Ao aparecer **Pronto ✓**, reinicie o VS Code; o aviso some sozinho.
17
+
18
+ Sem conexão, o botão mostra um pedido para copiar no Claude Code ou no Codex.
19
+ Cole, aprove quando a IA pedir e acompanhe: ela executa o comando abaixo. Em
20
+ programas antigos, o botão atualiza os plugins e mostra esse mesmo painel para
21
+ completar a preparação. “Ver detalhes” explica o motivo do aviso e “Agora não”
22
+ o dispensa nesta sessão.
23
+
11
24
  ## Instalar (equipe — um comando, sem clonar nada)
12
25
 
13
26
  ```sh
14
- claude mcp add --scope user gestao -- npx -y chatflux-gestao-agente mcp
27
+ npx -y chatflux-gestao-agente@latest configurar
15
28
  ```
16
29
 
17
- Pronto. Abra o Claude Code e peça qualquer coisa do portal: na primeira vez ele
18
- mesmo abre o navegador para você autorizar. **Você não digita mais nenhum
19
- comando** — nem `login`, nem `status`.
30
+ No Terminal do Mac ou PowerShell do Windows, esse comando instala a cópia
31
+ permanente do programa, confere a loja oficial de plugins do Claude Code e do
32
+ Codex, atualiza o `chatflux-app`, renova o login quando necessário (abre o
33
+ navegador) e registra/liga o computador. Ferramentas ausentes são puladas; cada
34
+ etapa mostra seu resultado e o que ainda falta fazer. Reinicie o VS Code ao final.
35
+
36
+ Para trocar uma loja antiga, ele pede confirmação. Use `--sim` para autorizar sem
37
+ perguntas, ou `--verificar` para apenas diagnosticar, sem instalar, atualizar,
38
+ abrir login ou ligar o computador. O toolkit é privado: sua conta do GitHub
39
+ precisa ter acesso a `Chatflux-Infra-Tools/chatflux-agent-toolkit`.
40
+
41
+ Depois da instalação, você também pode rodar `gestao configurar`. Se o npm negar
42
+ permissão para instalar globalmente, configure o Node/npm em uma pasta do seu
43
+ usuário e repita, sem sudo nem administrador. Login e ligação do computador
44
+ usam sempre a cópia permanente, nunca o cache temporário do npx.
45
+
46
+ Para registrar o conector MCP diretamente no Claude Code:
47
+
48
+ ```sh
49
+ claude mcp add --scope user gestao -- gestao mcp
50
+ ```
20
51
 
21
52
  ### Instalar (desenvolvimento, dentro deste repo)
22
53
 
package/bin/gestao.mjs CHANGED
@@ -14,6 +14,7 @@ const AJUDA = `gestao — portal Gestão pela linha de comando e por agente (MCP
14
14
 
15
15
  gestao login autoriza este computador a agir como você (abre o navegador)
16
16
  gestao status mostra quem está conectado e quanto tempo falta
17
+ gestao configurar [--verificar] [--sim] prepara plugins, login e computador
17
18
  gestao ligar instala início automático e associa este computador ao navegador
18
19
  gestao desligar remove início automático e interrompe o computador, mesmo offline
19
20
  gestao computador executar residente técnico (usado pelo login do SO)
@@ -30,6 +31,11 @@ const [, , verbo] = process.argv;
30
31
 
31
32
  try {
32
33
  switch (verbo) {
34
+ case "configurar": {
35
+ const { configurar } = await import("../lib/configurar.mjs");
36
+ await configurar(process.argv.slice(3));
37
+ break;
38
+ }
33
39
  case "login": {
34
40
  if (nomeServico()) {
35
41
  console.error("Modo serviço (GESTAO_SERVICO) ativo: não há login de pessoa. Use `gestao status`.");
@@ -49,6 +49,7 @@ const privateResult = x => ({
49
49
  baixar_abrir: shape({ kind: oneOf('baixar_abrir'), canonicalPath: text, ...opening }),
50
50
  escolher_pasta_projeto: shape({ kind: oneOf('escolher_pasta_projeto'), canonicalPath: text, ...opening }),
51
51
  escolher_pasta_padrao: shape({ kind: oneOf('escolher_pasta_padrao'), canonicalPath: text }),
52
+ configurar: shape({ kind: oneOf('configurar'), items: x => Array.isArray(x) && x.length > 0 && x.length <= 20 && x.every(shape({ status: oneOf('ok', 'erro', 'aviso'), item: x => text(x) && x.length <= 100, text: x => text(x) && x.length <= 600 })), restartRequired: bool }),
52
53
  atualizar_plugin: shape({ kind: oneOf('atualizar_plugin'), claude: plugin, codex: plugin, restartRequired: bool }),
53
54
  responder_sessao: shape({ kind: oneOf('responder_sessao'), turnStatus: oneOf('encerrado', 'cancelado', 'timeout', 'incerto'), resposta: optional(x => typeof x === 'string' && x.length <= 4000), motivo: optional(oneOf('conversa_em_uso', 'editor_aberto', 'pasta_invalida', 'outro')) }),
54
55
  ler_historico: shape({ kind: oneOf('ler_historico'), count: x => uint(x) && x <= 30, partial: bool, digest }),
@@ -6,13 +6,14 @@
6
6
  * F02/Windows/Codex continuam atrás de capabilities e das receitas injetadas A4.
7
7
  */
8
8
  import path from 'node:path';
9
+ import { configurar } from './configurar.mjs';
9
10
  import { readFileSync } from 'node:fs';
10
11
  import { acquireInstanceLock } from './computador-estado.mjs';
11
12
  import { readUserJson } from './computador-identidade.mjs';
12
13
  import { createReplyAdapter } from './computador-resposta.mjs';
13
14
 
14
15
  const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))).version;
15
- const ACTIONS = ['abrir_pasta', 'abrir_sessao', 'baixar_abrir', 'escolher_pasta_projeto', 'escolher_pasta_padrao', 'atualizar_plugin', 'responder_sessao', 'ler_historico'];
16
+ const ACTIONS = ['abrir_pasta', 'abrir_sessao', 'baixar_abrir', 'escolher_pasta_projeto', 'escolher_pasta_padrao', 'atualizar_plugin', 'configurar', 'responder_sessao', 'ler_historico'];
16
17
  const OPENING = ['abrindo_pasta', 'aguardando_editor', 'abrindo_conversa'];
17
18
  const RETRIES = [2000, 5000, 15000, 30000, 60000, 120000];
18
19
  const CLAIM = 'ops.computerRequestActions.claim', REPORT = 'ops.computerRequestActions.report';
@@ -48,7 +49,7 @@ function errorType(e) {
48
49
  return 'outro';
49
50
  }
50
51
 
51
- export async function runComputer({ identity, portal, journal, paths, folders, editor, git, plugins, diagnostics, history, reply = createReplyAdapter(), clock = {}, signal }) {
52
+ export async function runComputer({ identity, portal, journal, paths, folders, editor, git, plugins, diagnostics, history, configure = configurar, reply = createReplyAdapter(), clock = {}, signal }) {
52
53
  const now = clock.now?.bind(clock) ?? Date.now;
53
54
  const later = clock.setTimeout?.bind(clock) ?? setTimeout, cancel = clock.clearTimeout?.bind(clock) ?? clearTimeout;
54
55
  const random = clock.random?.bind(clock) ?? Math.random;
@@ -520,6 +521,21 @@ export async function runComputer({ identity, portal, journal, paths, folders, e
520
521
  }, () => ({ kind: 'concluir', privateResult: { kind: 'abrir_pasta' } }));
521
522
  return r;
522
523
  }
524
+ if (r.action === 'configurar') {
525
+ await phase('atualizando_plugin');
526
+ await effect('atualizando_plugin', () => configure(['--sim'], {
527
+ nonInteractive: true, signal: operationSignal, announce: () => {},
528
+ }), result => {
529
+ const short = (value, limit) => String(value ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, limit);
530
+ const items = result.results.map(row => ({ status: row.ok === true ? 'ok' : row.ok === 'warn' ? 'aviso' : 'erro',
531
+ item: short(row.item, 100), text: short(row.message, 600) }));
532
+ const privateResult = { kind: 'configurar', items, restartRequired: result.restartRequired === true };
533
+ return items.some(row => row.status !== 'ok')
534
+ ? { kind: 'aguardar', choices: ['tentar_novamente', 'cancelar'], privateResult }
535
+ : { kind: 'concluir', privateResult };
536
+ });
537
+ return r;
538
+ }
523
539
  if (r.action === 'atualizar_plugin') {
524
540
  if (!capabilities.atualizarPlugin) throw fail('EDITOR_RECIPE_UNVERIFIED');
525
541
  await phase('atualizando_plugin');
@@ -7,14 +7,14 @@
7
7
  */
8
8
  import { spawn } from 'node:child_process';
9
9
 
10
- const PLUGIN = 'chatflux-app@chatflux-marketplace';
11
- const MARKETPLACE = 'chatflux-marketplace';
10
+ export const PLUGIN = 'chatflux-app@chatflux-marketplace';
11
+ export const MARKETPLACE = 'chatflux-marketplace';
12
12
  const MAC_BINARIES = { claude: '/opt/homebrew/bin/claude', codex: '/opt/homebrew/bin/codex' };
13
- const LIST = {
13
+ export const LIST = {
14
14
  claude: ['plugin', 'list', '--json'],
15
15
  codex: ['plugin', 'list', '--marketplace', MARKETPLACE, '--json'],
16
16
  };
17
- const UPDATES = {
17
+ export const UPDATES = {
18
18
  claude: [['plugin', 'marketplace', 'update', MARKETPLACE], ['plugin', 'update', PLUGIN]],
19
19
  codex: [['plugin', 'marketplace', 'upgrade', MARKETPLACE]],
20
20
  };
@@ -28,11 +28,13 @@ const failed = (pluginVersion = null, exitCode) => ({
28
28
 
29
29
  function dependencies(options, updating = false) {
30
30
  const allowed = ['signal', 'spawnImpl', 'platform', 'windowsAdapter', 'timeoutMs'];
31
- if (updating) allowed.push('previousResults');
31
+ if (updating) allowed.push('previousResults', 'tools', 'marketplaceUpdated');
32
32
  if (!options || Object.keys(options).some(k => !allowed.includes(k))) throw new Error('Opções de plugin inválidas.');
33
33
  const { spawnImpl = spawn, platform = process.platform, timeoutMs = 120_000, windowsAdapter, signal } = options;
34
34
  if (typeof spawnImpl !== 'function' || (windowsAdapter !== undefined && typeof windowsAdapter !== 'function') ||
35
35
  !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 120_000) throw new Error('Opções de plugin inválidas.');
36
+ if (updating && ['tools', 'marketplaceUpdated'].some(key => options[key] !== undefined &&
37
+ (!Array.isArray(options[key]) || options[key].some(tool => !['claude', 'codex'].includes(tool))))) throw new Error('Opções de plugin inválidas.');
36
38
  return { spawnImpl, platform, timeoutMs, windowsAdapter, signal };
37
39
  }
38
40
 
@@ -86,7 +88,7 @@ function run(tool, argv, deps, capture = false) {
86
88
  });
87
89
  }
88
90
 
89
- function installedVersion(tool, stdout) {
91
+ export function installedVersion(tool, stdout) {
90
92
  try {
91
93
  const data = JSON.parse(stdout);
92
94
  const rows = tool === 'claude' ? data : data.installed;
@@ -111,7 +113,7 @@ export async function readPluginVersions(options = {}) {
111
113
  return { claudePluginVersion: claude.pluginVersion, codexPluginVersion: codex.pluginVersion };
112
114
  }
113
115
 
114
- async function update(tool, previous, deps) {
116
+ async function update(tool, previous, deps, marketplaceUpdated = []) {
115
117
  // Só sucesso é terminal na retentativa. Projeção explícita descarta campos livres
116
118
  // mesmo se o journal antigo contiver dados além do protocolo B1.
117
119
  if (previous?.status === 'atualizado') return { status: 'atualizado', pluginVersion: version(previous.pluginVersion) };
@@ -119,6 +121,7 @@ async function update(tool, previous, deps) {
119
121
  if (before.missing) return { status: 'nao_instalado' };
120
122
  if (!before.ok || !before.pluginVersion) return failed(before.pluginVersion, before.exitCode);
121
123
  for (const argv of UPDATES[tool]) {
124
+ if (marketplaceUpdated.includes(tool) && argv[1] === 'marketplace') continue;
122
125
  const result = await run(tool, argv, deps);
123
126
  if (!result.ok) return failed(before.pluginVersion, result.exitCode);
124
127
  }
@@ -129,8 +132,9 @@ async function update(tool, previous, deps) {
129
132
 
130
133
  export async function updatePlugins(options = {}) {
131
134
  const deps = dependencies(options, true);
132
- const claude = await update('claude', options.previousResults?.claude, deps);
133
- const codex = await update('codex', options.previousResults?.codex, deps);
135
+ const tools = options.tools ?? ['claude', 'codex'];
136
+ const claude = tools.includes('claude') ? await update('claude', options.previousResults?.claude, deps, options.marketplaceUpdated) : { status: 'nao_instalado' };
137
+ const codex = tools.includes('codex') ? await update('codex', options.previousResults?.codex, deps, options.marketplaceUpdated) : { status: 'nao_instalado' };
134
138
  // Conservador mesmo em sucesso parcial: o comando pode ter alterado arquivos
135
139
  // antes da falha. Reinício é orientação; esta API não encerra editor/conversa.
136
140
  return { claude, codex, restartRequired: options.previousResults?.restartRequired === true || deps.updateSent === true || claude.status === 'atualizado' || codex.status === 'atualizado' };
@@ -0,0 +1,326 @@
1
+ /** Preparação pessoal com receita fixa, também usada pelo botão do portal. Efeitos só fora de --verificar. */
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import http from 'node:http';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { spawn } from 'node:child_process';
8
+ import { createInterface } from 'node:readline/promises';
9
+ import { lerToken, expiraEm } from './config.mjs';
10
+ import { scopeDirectory } from './computador-identidade.mjs';
11
+ import { LIST, UPDATES, MARKETPLACE, PLUGIN, installedVersion, updatePlugins } from './computador-plugin.mjs';
12
+
13
+ const REPO = 'Chatflux-Infra-Tools/chatflux-agent-toolkit';
14
+ const URL_GIT = `https://github.com/${REPO}.git`;
15
+ const ENTRY = fileURLToPath(new URL('../bin/gestao.mjs', import.meta.url));
16
+ const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))).version;
17
+ // Mesmo predicado cached de computador-startup.mjs; aquele módulo não o exporta.
18
+ const cached = value => /(?:^|[\\/])(?:_npx|_cacache)(?:[\\/]|$)/i.test(value);
19
+ const names = { claude: 'Claude Code', codex: 'Codex' };
20
+ const accessHelp = 'Confira seu acesso ao repositório privado Chatflux-Infra-Tools/chatflux-agent-toolkit no GitHub e a conexão; depois rode gestao configurar de novo.';
21
+
22
+ async function ask(question) {
23
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
24
+ const input = createInterface({ input: process.stdin, output: process.stdout });
25
+ try { return /^(s|sim)$/i.test((await input.question(`${question} `)).trim()); }
26
+ finally { input.close(); }
27
+ }
28
+
29
+ export async function configurar(flags = [], options = {}) {
30
+ if (flags.some(flag => !['--sim', '--verificar'].includes(flag))) throw new Error('Use gestao configurar [--sim] [--verificar].');
31
+ const verify = flags.includes('--verificar'), yes = flags.includes('--sim');
32
+ const signal = options.signal;
33
+ signal?.throwIfAborted();
34
+ const platform = options.platform ?? process.platform;
35
+ if (!['darwin', 'win32'].includes(platform)) throw new Error('Configurar está disponível para Mac e Windows.');
36
+ const p = platform === 'win32' ? path.win32 : path.posix;
37
+ const fsImpl = options.fsImpl ?? fs, spawnImpl = options.spawnImpl ?? spawn;
38
+ const home = options.home ?? os.homedir(), env = options.env ?? process.env;
39
+ const environment = env.GESTAO_ENV === 'dev' ? 'dev' : 'prod';
40
+ const commandEnv = { ...env, GESTAO_ENV: environment };
41
+ const nodePath = options.nodePath ?? process.execPath, entryPath = fsImpl.realpathSync(options.entryPath ?? ENTRY);
42
+ const version = options.version ?? VERSION, now = options.now ?? Date.now;
43
+ const announce = options.announce ?? console.log, confirm = options.confirm ?? ask;
44
+ const readToken = options.readToken ?? lerToken;
45
+ const results = [];
46
+ let restart = false;
47
+ function result(item, ok, message) {
48
+ signal?.throwIfAborted();
49
+ const line = `${item}: ${message} ${ok === 'warn' ? '⚠' : ok ? '✓' : '✗'}`;
50
+ results.push({ item, ok, message }); announce(line);
51
+ }
52
+ function read(file) {
53
+ try { return fsImpl.readFileSync(file, 'utf8'); }
54
+ catch (e) { if (e.code === 'ENOENT') return null; throw e; }
55
+ }
56
+ const json = file => { const text = read(file); return text === null ? null : JSON.parse(text); };
57
+ function resolveCommand(tool) {
58
+ const directories = [...new Set([...(env.PATH ?? env.Path ?? '').split(platform === 'win32' ? ';' : ':'),
59
+ p.dirname(nodePath), p.join(home, '.local/bin'), ...(platform === 'darwin' ? ['/opt/homebrew/bin', '/usr/local/bin'] : [])])].filter(Boolean);
60
+ for (const directory of directories) {
61
+ for (const ext of platform === 'win32' ? ['.exe', '.cmd'] : ['']) {
62
+ const binary = p.join(directory, `${tool}${ext}`);
63
+ try {
64
+ fsImpl.accessSync(binary, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
65
+ if (!fsImpl.statSync(binary).isFile()) continue;
66
+ if (ext !== '.cmd') return { binary, prefix: [] };
67
+ // npm gera .cmd que não são executáveis via execFile. Rodar seu JS com
68
+ // o Node evita shell, aspas e expansão de %/& em caminhos do Windows.
69
+ const script = p.join(directory, 'node_modules', { npm: 'npm/bin/npm-cli.js', codex: '@openai/codex/bin/codex.js', claude: '@anthropic-ai/claude-code/cli.js' }[tool]);
70
+ if (fsImpl.existsSync(script)) return { binary: nodePath, prefix: [script] };
71
+ } catch (e) { if (!['ENOENT', 'EACCES'].includes(e.code)) throw e; }
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ const commands = {};
77
+ function start(command, argv, spawnOptions = {}) {
78
+ signal?.throwIfAborted();
79
+ if (!command) throw new Error('Programa não encontrado.');
80
+ return spawnImpl(command.binary, [...command.prefix, ...argv], { ...spawnOptions, ...(command.cwd ? { cwd: command.cwd } : {}), env: commandEnv, shell: false, windowsHide: true });
81
+ }
82
+ async function run(command, argv, interactive = false) {
83
+ return new Promise((resolve, reject) => {
84
+ let child, timer, bytes = 0, stdout = '', stderr = '', done = false;
85
+ const finish = (error, value) => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener('abort', abort); error ? reject(error) : resolve(value); };
86
+ const abort = () => { try { child?.kill('SIGKILL'); } catch { /* somente filho próprio */ } finish(signal.reason ?? new Error('Cancelado.')); };
87
+ const stop = () => { try { child?.kill('SIGKILL'); } catch { /* somente filho próprio */ } finish(new Error('O comando demorou demais; tente novamente.')); };
88
+ try {
89
+ child = start(command, argv, { stdio: interactive && !options.nonInteractive ? 'inherit' : ['ignore', 'pipe', 'pipe'] });
90
+ signal?.addEventListener('abort', abort, { once: true });
91
+ if (signal?.aborted) { abort(); return; }
92
+ for (const stream of ['stdout', 'stderr']) child[stream]?.on('data', chunk => {
93
+ bytes += Buffer.byteLength(chunk); if (bytes > 1024 * 1024) return stop();
94
+ if (stream === 'stdout') stdout += chunk; else stderr += chunk;
95
+ });
96
+ child.once('error', error => finish(error));
97
+ child.once('close', code => code === 0 ? finish(null, stdout.trim()) : finish(Object.assign(new Error('Não foi possível concluir o comando.'), { permission: /EACCES|EPERM|permission denied/i.test(stderr) })));
98
+ timer = setTimeout(stop, interactive ? 5 * 60_000 : 120_000);
99
+ } catch (error) { finish(error); }
100
+ });
101
+ }
102
+
103
+ announce(`Conferindo este computador · ambiente ${environment}${verify ? ' · somente diagnóstico' : ''}.`);
104
+ // Nada instala startup ou abre login com o ENTRY efêmero do npx.
105
+ let persistent = entryPath;
106
+ if (verify) {
107
+ try {
108
+ commands.npm = resolveCommand('npm');
109
+ const root = await run(commands.npm, ['root', '-g']);
110
+ if (!p.isAbsolute(root)) throw new Error('Pasta global inválida.');
111
+ const installed = json(p.join(root, 'chatflux-gestao-agente/package.json'))?.version;
112
+ let latest = null;
113
+ try { latest = await run(commands.npm, ['view', 'chatflux-gestao-agente', 'version']); } catch { /* sem registro */ }
114
+ const valid = v => typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v);
115
+ if (!valid(installed)) result('Programa do portal', false, 'instalação permanente não encontrada');
116
+ else if (!valid(latest)) result('Programa do portal', 'warn', `${installed}; versão do npm não confirmada`);
117
+ else if (installed !== latest) result('Programa do portal', false, `${installed} → ${latest} disponível`);
118
+ else result('Programa do portal', true, installed);
119
+ } catch { result('Programa do portal', 'warn', 'instalação permanente não confirmada'); }
120
+ } else if (cached(entryPath)) {
121
+ persistent = null;
122
+ try {
123
+ commands.npm = resolveCommand('npm');
124
+ await run(commands.npm, ['install', '-g', `chatflux-gestao-agente@${version}`]);
125
+ const root = await run(commands.npm, ['root', '-g']);
126
+ if (!p.isAbsolute(root)) throw new Error('Pasta global inválida.');
127
+ const candidate = fsImpl.realpathSync(p.join(root, 'chatflux-gestao-agente/bin/gestao.mjs'));
128
+ if (cached(candidate) || !fsImpl.statSync(candidate).isFile() || json(p.join(root, 'chatflux-gestao-agente/package.json'))?.version !== version) throw new Error('Instalação permanente não confirmada.');
129
+ persistent = candidate;
130
+ result('Programa do portal instalado', true, version);
131
+ } catch (error) {
132
+ const permission = error.permission || ['EACCES', 'EPERM'].includes(error.code);
133
+ result('Programa do portal instalado', false, permission
134
+ ? 'Sem permissão para instalar. Configure o npm/Node em uma pasta do seu usuário e repita gestao configurar, sem sudo nem administrador.'
135
+ : 'Instalação não confirmada. Confira o npm e a conexão; repita gestao configurar.');
136
+ }
137
+ } else result('Programa do portal instalado', true, version);
138
+
139
+ for (const tool of ['claude', 'codex']) {
140
+ try { commands[tool] = resolveCommand(tool); result(names[tool], true, commands[tool] ? 'instalado' : 'não instalado; etapa pulada'); }
141
+ catch { result(names[tool], false, 'não foi possível localizar o programa'); }
142
+ }
143
+ function source(tool) {
144
+ if (tool === 'claude') {
145
+ const data = json(p.join(home, '.claude/plugins/known_marketplaces.json'));
146
+ const entry = data?.[MARKETPLACE];
147
+ return { exists: !!entry, official: entry?.source?.source === 'github' && entry.source.repo === REPO };
148
+ }
149
+ const text = read(p.join(env.CODEX_HOME ?? p.join(home, '.codex'), 'config.toml')) ?? '';
150
+ let inside = false, exists = false;
151
+ const values = {};
152
+ for (const line of text.split(/\r?\n/)) {
153
+ const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
154
+ if (section) {
155
+ inside = /^marketplaces\.(?:chatflux-marketplace|"chatflux-marketplace"|'chatflux-marketplace')$/.test(section[1].trim());
156
+ if (inside) { if (exists) throw new Error('Loja duplicada no Codex.'); exists = true; }
157
+ } else if (inside && /^\s*source(?:_type)?\s*=/.test(line)) {
158
+ const field = line.match(/^\s*(source_type|source)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/);
159
+ if (!field || Object.hasOwn(values, field[1])) throw new Error('Fonte do Codex não reconhecida.');
160
+ values[field[1]] = field[2][0] === '"' ? JSON.parse(field[2]) : field[2].slice(1, -1);
161
+ }
162
+ }
163
+ return { exists, official: values.source_type === 'git' && values.source === URL_GIT };
164
+ }
165
+ const snapshots = {}, ready = [];
166
+ for (const tool of ['claude', 'codex']) {
167
+ if (!commands[tool]) continue;
168
+ try {
169
+ const store = source(tool);
170
+ if (verify) {
171
+ result(`${names[tool]} — loja de plugins`, store.official, store.official ? 'fonte oficial; pode atualizar' : 'falta trocar para a fonte oficial');
172
+ if (tool === 'claude') {
173
+ const installed = json(p.join(home, '.claude/plugins/installed_plugins.json'));
174
+ const rows = installed?.plugins?.[PLUGIN];
175
+ if (Array.isArray(rows)) snapshots[tool] = { version: installedVersion(tool, JSON.stringify(rows.map(row => ({ ...row, id: PLUGIN })))) };
176
+ } else try { snapshots[tool] = { version: installedVersion(tool, await run(commands[tool], LIST[tool])) }; } catch { /* versão desconhecida */ }
177
+ continue;
178
+ }
179
+ const output = await run(commands[tool], LIST[tool]);
180
+ const data = JSON.parse(output), rows = tool === 'claude' ? data : data.installed;
181
+ if (!Array.isArray(rows)) throw new Error('Lista de plugins não reconhecida.');
182
+ snapshots[tool] = { output, version: installedVersion(tool, output) };
183
+ const plugins = rows.filter(row => tool === 'claude' ? row?.id?.endsWith(`@${MARKETPLACE}`) : row?.installed === true && row.pluginId?.endsWith(`@${MARKETPLACE}`))
184
+ .map(row => ({ id: tool === 'claude' ? row.id : row.pluginId, scope: tool === 'claude' ? row.scope ?? 'user' : 'user', cwd: row.projectPath }));
185
+ if (!plugins.some(row => row.id === PLUGIN && row.scope === 'user')) plugins.push({ id: PLUGIN, scope: 'user' });
186
+ if (plugins.some(row => !/^[a-zA-Z0-9_-]+@chatflux-marketplace$/.test(row.id) || !['user', 'project', 'local'].includes(row.scope) || (row.scope !== 'user' && !p.isAbsolute(row.cwd ?? '')))) throw new Error('Não foi possível preservar os plugins instalados.');
187
+ if (!store.official) {
188
+ if (!yes && !await confirm('Trocar a loja de plugins para a oficial? (s/n)')) {
189
+ result(`${names[tool]} — loja de plugins`, false, 'troca não autorizada; repita no terminal ou use --sim para autorizar'); continue;
190
+ }
191
+ restart = true;
192
+ if (store.exists) await run(commands[tool], ['plugin', 'marketplace', 'remove', MARKETPLACE]);
193
+ await run(commands[tool], ['plugin', 'marketplace', 'add', tool === 'claude' ? REPO : URL_GIT]);
194
+ }
195
+ restart = true;
196
+ await run(commands[tool], UPDATES[tool][0]);
197
+ ready.push(tool);
198
+ const missingApp = !rows.some(row => tool === 'claude' ? row.id === PLUGIN && row.scope === 'user' : row.pluginId === PLUGIN && row.installed);
199
+ let restored = true;
200
+ for (const row of !store.official ? plugins : missingApp ? [{ id: PLUGIN, scope: 'user' }] : []) {
201
+ try {
202
+ // O escopo de projeto usa o diretório original, sem alterar o cwd do processo.
203
+ await run({ ...commands[tool], cwd: row.cwd }, tool === 'claude' ? ['plugin', 'install', row.id, '--scope', row.scope] : ['plugin', 'add', row.id]);
204
+ } catch { restored = false; }
205
+ }
206
+ result(`${names[tool]} — loja de plugins`, restored, restored ? 'fonte oficial atualizada; plugins preservados' : `fonte oficial atualizada; falta reinstalar um plugin. ${accessHelp}`);
207
+ } catch {
208
+ result(`${names[tool]} — loja de plugins`, false, `Não foi possível conferir ou atualizar a loja. ${accessHelp}`);
209
+ }
210
+ }
211
+ if (verify) {
212
+ let latest = null, origin = '';
213
+ try {
214
+ const data = readToken();
215
+ if (data?.env === environment && expiraEm(data.token) > now()) {
216
+ const host = environment === 'dev' ? 'https://agreeable-woodpecker-190.convex.cloud' : 'https://dependable-stingray-656.convex.cloud';
217
+ const response = await (options.fetchImpl ?? fetch)(`${host}/api/query`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${data.token}` }, body: JSON.stringify({ path: 'ops/plugin:meuStatus', args: {}, format: 'json' }), signal: AbortSignal.timeout(10000) });
218
+ const body = await response.json();
219
+ if (response.ok && body.status === 'success') latest = body.value?.atual;
220
+ }
221
+ } catch { /* tenta loja local */ }
222
+ if (!/^\d+\.\d+\.\d+$/.test(latest ?? '')) {
223
+ try { latest = json(p.join(home, '.claude/plugins/marketplaces/chatflux-marketplace/plugins/chatflux-app/.claude-plugin/plugin.json'))?.version; origin = ' (versão da loja)'; } catch { /* indisponível */ }
224
+ }
225
+ if (!/^\d+\.\d+\.\d+$/.test(latest ?? '')) latest = null;
226
+ for (const tool of ['claude', 'codex']) if (commands[tool]) {
227
+ const installed = snapshots[tool]?.version;
228
+ if (!installed) result(`${names[tool]} — chatflux-app`, 'warn', `versão não confirmada${latest && origin ? `; versão da loja: ${latest}` : ''}`);
229
+ else if (!latest) result(`${names[tool]} — chatflux-app`, 'warn', `${installed}; versão atual não confirmada`);
230
+ else if (installed !== latest) result(`${names[tool]} — chatflux-app`, false, `${installed} → atual ${latest}${origin} (vai atualizar)`);
231
+ else result(`${names[tool]} — chatflux-app`, true, `${installed}${origin}`);
232
+ }
233
+ } else {
234
+ const bridge = (tool, argv, spawnOptions) => start(commands[tool], argv, spawnOptions);
235
+ const updated = await updatePlugins({ platform, signal, tools: ready, marketplaceUpdated: ready,
236
+ ...(platform === 'win32' ? { windowsAdapter: bridge } : { spawnImpl: (binary, argv, spawnOptions) => bridge(binary.endsWith('/claude') ? 'claude' : 'codex', argv, spawnOptions) }) });
237
+ restart ||= updated.restartRequired;
238
+ for (const tool of ['claude', 'codex']) if (commands[tool]) {
239
+ const item = updated[tool], ok = ready.includes(tool) && item.status === 'atualizado';
240
+ result(`${names[tool]} — chatflux-app`, ok, ok ? item.pluginVersion : `atualização não confirmada${item.pluginVersion ? `; versão ${item.pluginVersion}` : ''}. ${accessHelp}`);
241
+ }
242
+ }
243
+
244
+ const credential = () => { const value = readToken(); return value?.env === environment ? value : null; };
245
+ const expiry = value => value?.token ? expiraEm(value.token) : 0;
246
+ const until = value => new Date(expiry(value)).toLocaleString('pt-BR');
247
+ let loginOk = false;
248
+ try {
249
+ let data = credential();
250
+ if (expiry(data) - now() <= 15 * 86400000) {
251
+ if (verify) result('Login do portal', false, 'ausente, vencido ou vence em até 15 dias; precisa renovar no navegador');
252
+ else if (!persistent) result('Login do portal', false, 'pulado; instale o programa do portal em uma pasta permanente');
253
+ else {
254
+ await run({ binary: nodePath, prefix: [persistent] }, ['login'], true);
255
+ data = credential(); loginOk = expiry(data) > now();
256
+ result('Login do portal', loginOk, loginOk ? `conectado até ${until(data)}` : 'login não confirmado; rode gestao login');
257
+ }
258
+ } else { loginOk = true; result('Login do portal', true, `válido até ${until(data)}`); }
259
+ } catch { result('Login do portal', false, 'não foi possível conectar; rode gestao login e autorize no navegador'); }
260
+
261
+ // statusComputer protege arquivos e openJournal cria diretórios. O diagnóstico
262
+ // usa apenas um retrato dos metadados, sem ACL, journal aberto ou canal local.
263
+ function readOnlyComputer() {
264
+ const baseDir = p.join(home, '.config/gestao/computadores');
265
+ const active = json(p.join(baseDir, environment, 'ativo.json'));
266
+ if (!active?.subject) return { computerId: null, startupInstalled: false, startupHealthy: false };
267
+ const directory = p.join(baseDir, environment, p.basename(scopeDirectory({ env: environment, subject: active.subject })));
268
+ const identity = json(p.join(directory, 'identidade.json'));
269
+ const journal = json(p.join(directory, 'journal.json'));
270
+ const startupRoot = platform === 'darwin' ? p.join(home, '.config/gestao/startup') : p.join(env.LOCALAPPDATA || home, 'ChatFluxGestao/startup');
271
+ const config = json(p.join(startupRoot, environment, 'startup.json'));
272
+ const launcher = platform === 'darwin'
273
+ ? p.join(home, 'Library/LaunchAgents', `ai.chatflux.gestao-computador.${environment}.plist`)
274
+ : p.join(startupRoot, environment, 'launcher.vbs');
275
+ const configured = !!config && config.env === environment && config.platform === platform && fsImpl.existsSync(launcher) &&
276
+ [config.nodePath, config.entryPath, config.identityPath].every(file => typeof file === 'string' && !cached(file) && fsImpl.existsSync(file));
277
+ return { directory, computerId: identity?.computerId, startupInstalled: configured, startupHealthy: configured,
278
+ state: journal?.stopped ? 'desligado' : 'configurado; execução não conferida' };
279
+ }
280
+ async function checkResident(directory) {
281
+ const file = p.join(directory, 'controle.json');
282
+ let stat;
283
+ try { stat = fsImpl.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
284
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 8192) throw new Error('Controle inválido.');
285
+ const data = json(file);
286
+ if (data?.version !== 1 || !Number.isSafeInteger(data.port) || data.port < 1 || data.port > 65535 || !Number.isSafeInteger(data.pid) || data.pid < 1 || !/^[a-f0-9]{64}$/.test(data.key)) throw new Error('Controle inválido.');
287
+ return new Promise((resolve, reject) => {
288
+ const req = http.request({ host: '127.0.0.1', port: data.port, path: '/status', method: 'POST', headers: { Authorization: `Bearer ${data.key}` }, timeout: 3000, agent: false }, res => {
289
+ let body = ''; res.setEncoding('utf8');
290
+ res.on('data', chunk => { body += chunk; if (body.length > 8192) req.destroy(); });
291
+ res.on('end', () => { try { if (res.statusCode !== 200) throw new Error(); resolve(JSON.parse(body)); } catch { reject(new Error('Controle indisponível.')); } });
292
+ });
293
+ req.on('timeout', () => req.destroy()); req.on('error', reject); req.end();
294
+ });
295
+ }
296
+ try {
297
+ const status = verify ? (options.readOnlyComputer ?? readOnlyComputer)() : await (options.statusComputer ?? (await import('./computador-cli.mjs')).statusComputer)();
298
+ if (verify && status.computerId && status.startupInstalled && status.startupHealthy && status.state !== 'desligado') {
299
+ let live;
300
+ try { live = await (options.checkResident ?? checkResident)(status.directory); }
301
+ catch { live = undefined; }
302
+ if (live?.state) result('Computador', ['conectado', 'residente iniciado'].includes(live.state) ? true : 'warn', `residente em execução; ${live.state}`);
303
+ else if (live === null) result('Computador', false, 'configurado; residente não está rodando');
304
+ else result('Computador', 'warn', 'configurado; não deu para conferir se está rodando');
305
+ } else {
306
+ const connected = !!status.computerId && status.startupInstalled && status.startupHealthy &&
307
+ ['conectado', 'residente iniciado'].includes(status.state);
308
+ if (connected) result('Computador', true, verify ? status.state : 'já registrado e ligado');
309
+ else if (verify) result('Computador', false, 'precisa registrar ou ligar; rode gestao configurar');
310
+ else if (!persistent || !loginOk) result('Computador', false, 'etapa pulada; conclua a instalação e o login, depois repita gestao configurar');
311
+ else {
312
+ await run({ binary: nodePath, prefix: [persistent] }, ['ligar'], true);
313
+ result('Computador', true, 'registrado e ligado');
314
+ }
315
+ }
316
+ } catch { result('Computador', false, 'não foi possível conferir ou ligar; rode gestao ligar e siga a orientação exibida'); }
317
+ const pending = results.filter(item => item.ok !== true);
318
+ if (!pending.length) announce('\nTudo pronto neste computador ✓');
319
+ else {
320
+ announce('\nResumo:');
321
+ for (const item of pending) announce(`${item.item}: ${item.message} ${item.ok === 'warn' ? '⚠' : '✗'}`);
322
+ }
323
+ if (restart) announce('Para terminar: reinicie o VS Code e abra uma nova conversa no Claude Code/Codex.');
324
+ if (verify && pending.length) announce('Para resolver, rode: npx -y chatflux-gestao-agente@latest configurar');
325
+ return { results, restartRequired: restart };
326
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatflux-gestao-agente",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Opere o portal Gestão da ChatFlux pelo Claude Code, Claude Desktop ou qualquer harness MCP — com a sua identidade e o seu papel.",
6
6
  "keywords": [