chatflux-gestao-agente 0.2.2 → 0.2.3

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
@@ -11,12 +11,30 @@ faz na tela, e tudo no histórico com o seu nome.
11
11
  ## Instalar (equipe — um comando, sem clonar nada)
12
12
 
13
13
  ```sh
14
- claude mcp add --scope user gestao -- npx -y chatflux-gestao-agente mcp
14
+ npx -y chatflux-gestao-agente@latest configurar
15
15
  ```
16
16
 
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`.
17
+ No Terminal do Mac ou PowerShell do Windows, esse comando instala a cópia
18
+ permanente do programa, confere a loja oficial de plugins do Claude Code e do
19
+ Codex, atualiza o `chatflux-app`, renova o login quando necessário (abre o
20
+ navegador) e registra/liga o computador. Ferramentas ausentes são puladas; cada
21
+ etapa mostra seu resultado e o que ainda falta fazer. Reinicie o VS Code ao final.
22
+
23
+ Para trocar uma loja antiga, ele pede confirmação. Use `--sim` para autorizar sem
24
+ perguntas, ou `--verificar` para apenas diagnosticar, sem instalar, atualizar,
25
+ abrir login ou ligar o computador. O toolkit é privado: sua conta do GitHub
26
+ precisa ter acesso a `Chatflux-Infra-Tools/chatflux-agent-toolkit`.
27
+
28
+ Depois da instalação, você também pode rodar `gestao configurar`. Se o npm negar
29
+ permissão para instalar globalmente, configure o Node/npm em uma pasta do seu
30
+ usuário e repita, sem sudo nem administrador. Login e ligação do computador
31
+ usam sempre a cópia permanente, nunca o cache temporário do npx.
32
+
33
+ Para registrar o conector MCP diretamente no Claude Code:
34
+
35
+ ```sh
36
+ claude mcp add --scope user gestao -- gestao mcp
37
+ ```
20
38
 
21
39
  ### Instalar (desenvolvimento, dentro deste repo)
22
40
 
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`.");
@@ -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,319 @@
1
+ /** Preparação pessoal, sem comandos recebidos 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 platform = options.platform ?? process.platform;
33
+ if (!['darwin', 'win32'].includes(platform)) throw new Error('Configurar está disponível para Mac e Windows.');
34
+ const p = platform === 'win32' ? path.win32 : path.posix;
35
+ const fsImpl = options.fsImpl ?? fs, spawnImpl = options.spawnImpl ?? spawn;
36
+ const home = options.home ?? os.homedir(), env = options.env ?? process.env;
37
+ const environment = env.GESTAO_ENV === 'dev' ? 'dev' : 'prod';
38
+ const commandEnv = { ...env, GESTAO_ENV: environment };
39
+ const nodePath = options.nodePath ?? process.execPath, entryPath = fsImpl.realpathSync(options.entryPath ?? ENTRY);
40
+ const version = options.version ?? VERSION, now = options.now ?? Date.now;
41
+ const announce = options.announce ?? console.log, confirm = options.confirm ?? ask;
42
+ const readToken = options.readToken ?? lerToken;
43
+ const results = [];
44
+ let restart = false;
45
+ function result(item, ok, message) {
46
+ const line = `${item}: ${message} ${ok === 'warn' ? '⚠' : ok ? '✓' : '✗'}`;
47
+ results.push({ item, ok, message }); announce(line);
48
+ }
49
+ function read(file) {
50
+ try { return fsImpl.readFileSync(file, 'utf8'); }
51
+ catch (e) { if (e.code === 'ENOENT') return null; throw e; }
52
+ }
53
+ const json = file => { const text = read(file); return text === null ? null : JSON.parse(text); };
54
+ function resolveCommand(tool) {
55
+ const directories = [...new Set([...(env.PATH ?? env.Path ?? '').split(platform === 'win32' ? ';' : ':'),
56
+ p.dirname(nodePath), p.join(home, '.local/bin'), ...(platform === 'darwin' ? ['/opt/homebrew/bin', '/usr/local/bin'] : [])])].filter(Boolean);
57
+ for (const directory of directories) {
58
+ for (const ext of platform === 'win32' ? ['.exe', '.cmd'] : ['']) {
59
+ const binary = p.join(directory, `${tool}${ext}`);
60
+ try {
61
+ fsImpl.accessSync(binary, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK);
62
+ if (!fsImpl.statSync(binary).isFile()) continue;
63
+ if (ext !== '.cmd') return { binary, prefix: [] };
64
+ // npm gera .cmd que não são executáveis via execFile. Rodar seu JS com
65
+ // o Node evita shell, aspas e expansão de %/& em caminhos do Windows.
66
+ 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]);
67
+ if (fsImpl.existsSync(script)) return { binary: nodePath, prefix: [script] };
68
+ } catch (e) { if (!['ENOENT', 'EACCES'].includes(e.code)) throw e; }
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+ const commands = {};
74
+ function start(command, argv, spawnOptions = {}) {
75
+ if (!command) throw new Error('Programa não encontrado.');
76
+ return spawnImpl(command.binary, [...command.prefix, ...argv], { ...spawnOptions, ...(command.cwd ? { cwd: command.cwd } : {}), env: commandEnv, shell: false, windowsHide: true });
77
+ }
78
+ async function run(command, argv, interactive = false) {
79
+ return new Promise((resolve, reject) => {
80
+ let child, timer, bytes = 0, stdout = '', stderr = '', done = false;
81
+ const finish = (error, value) => { if (done) return; done = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
82
+ const stop = () => { try { child?.kill('SIGKILL'); } catch { /* somente filho próprio */ } finish(new Error('O comando demorou demais; tente novamente.')); };
83
+ try {
84
+ child = start(command, argv, { stdio: interactive ? 'inherit' : ['ignore', 'pipe', 'pipe'] });
85
+ for (const stream of ['stdout', 'stderr']) child[stream]?.on('data', chunk => {
86
+ bytes += Buffer.byteLength(chunk); if (bytes > 1024 * 1024) return stop();
87
+ if (stream === 'stdout') stdout += chunk; else stderr += chunk;
88
+ });
89
+ child.once('error', error => finish(error));
90
+ 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) })));
91
+ timer = setTimeout(stop, interactive ? 5 * 60_000 : 120_000);
92
+ } catch (error) { finish(error); }
93
+ });
94
+ }
95
+
96
+ announce(`Conferindo este computador · ambiente ${environment}${verify ? ' · somente diagnóstico' : ''}.`);
97
+ // Nada instala startup ou abre login com o ENTRY efêmero do npx.
98
+ let persistent = entryPath;
99
+ if (verify) {
100
+ try {
101
+ commands.npm = resolveCommand('npm');
102
+ const root = await run(commands.npm, ['root', '-g']);
103
+ if (!p.isAbsolute(root)) throw new Error('Pasta global inválida.');
104
+ const installed = json(p.join(root, 'chatflux-gestao-agente/package.json'))?.version;
105
+ let latest = null;
106
+ try { latest = await run(commands.npm, ['view', 'chatflux-gestao-agente', 'version']); } catch { /* sem registro */ }
107
+ const valid = v => typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v);
108
+ if (!valid(installed)) result('Programa do portal', false, 'instalação permanente não encontrada');
109
+ else if (!valid(latest)) result('Programa do portal', 'warn', `${installed}; versão do npm não confirmada`);
110
+ else if (installed !== latest) result('Programa do portal', false, `${installed} → ${latest} disponível`);
111
+ else result('Programa do portal', true, installed);
112
+ } catch { result('Programa do portal', 'warn', 'instalação permanente não confirmada'); }
113
+ } else if (cached(entryPath)) {
114
+ persistent = null;
115
+ try {
116
+ commands.npm = resolveCommand('npm');
117
+ await run(commands.npm, ['install', '-g', `chatflux-gestao-agente@${version}`]);
118
+ const root = await run(commands.npm, ['root', '-g']);
119
+ if (!p.isAbsolute(root)) throw new Error('Pasta global inválida.');
120
+ const candidate = fsImpl.realpathSync(p.join(root, 'chatflux-gestao-agente/bin/gestao.mjs'));
121
+ 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.');
122
+ persistent = candidate;
123
+ result('Programa do portal instalado', true, version);
124
+ } catch (error) {
125
+ const permission = error.permission || ['EACCES', 'EPERM'].includes(error.code);
126
+ result('Programa do portal instalado', false, permission
127
+ ? 'Sem permissão para instalar. Configure o npm/Node em uma pasta do seu usuário e repita gestao configurar, sem sudo nem administrador.'
128
+ : 'Instalação não confirmada. Confira o npm e a conexão; repita gestao configurar.');
129
+ }
130
+ } else result('Programa do portal instalado', true, version);
131
+
132
+ for (const tool of ['claude', 'codex']) {
133
+ try { commands[tool] = resolveCommand(tool); result(names[tool], true, commands[tool] ? 'instalado' : 'não instalado; etapa pulada'); }
134
+ catch { result(names[tool], false, 'não foi possível localizar o programa'); }
135
+ }
136
+ function source(tool) {
137
+ if (tool === 'claude') {
138
+ const data = json(p.join(home, '.claude/plugins/known_marketplaces.json'));
139
+ const entry = data?.[MARKETPLACE];
140
+ return { exists: !!entry, official: entry?.source?.source === 'github' && entry.source.repo === REPO };
141
+ }
142
+ const text = read(p.join(env.CODEX_HOME ?? p.join(home, '.codex'), 'config.toml')) ?? '';
143
+ let inside = false, exists = false;
144
+ const values = {};
145
+ for (const line of text.split(/\r?\n/)) {
146
+ const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
147
+ if (section) {
148
+ inside = /^marketplaces\.(?:chatflux-marketplace|"chatflux-marketplace"|'chatflux-marketplace')$/.test(section[1].trim());
149
+ if (inside) { if (exists) throw new Error('Loja duplicada no Codex.'); exists = true; }
150
+ } else if (inside && /^\s*source(?:_type)?\s*=/.test(line)) {
151
+ const field = line.match(/^\s*(source_type|source)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/);
152
+ if (!field || Object.hasOwn(values, field[1])) throw new Error('Fonte do Codex não reconhecida.');
153
+ values[field[1]] = field[2][0] === '"' ? JSON.parse(field[2]) : field[2].slice(1, -1);
154
+ }
155
+ }
156
+ return { exists, official: values.source_type === 'git' && values.source === URL_GIT };
157
+ }
158
+ const snapshots = {}, ready = [];
159
+ for (const tool of ['claude', 'codex']) {
160
+ if (!commands[tool]) continue;
161
+ try {
162
+ const store = source(tool);
163
+ if (verify) {
164
+ result(`${names[tool]} — loja de plugins`, store.official, store.official ? 'fonte oficial; pode atualizar' : 'falta trocar para a fonte oficial');
165
+ if (tool === 'claude') {
166
+ const installed = json(p.join(home, '.claude/plugins/installed_plugins.json'));
167
+ const rows = installed?.plugins?.[PLUGIN];
168
+ if (Array.isArray(rows)) snapshots[tool] = { version: installedVersion(tool, JSON.stringify(rows.map(row => ({ ...row, id: PLUGIN })))) };
169
+ } else try { snapshots[tool] = { version: installedVersion(tool, await run(commands[tool], LIST[tool])) }; } catch { /* versão desconhecida */ }
170
+ continue;
171
+ }
172
+ const output = await run(commands[tool], LIST[tool]);
173
+ const data = JSON.parse(output), rows = tool === 'claude' ? data : data.installed;
174
+ if (!Array.isArray(rows)) throw new Error('Lista de plugins não reconhecida.');
175
+ snapshots[tool] = { output, version: installedVersion(tool, output) };
176
+ const plugins = rows.filter(row => tool === 'claude' ? row?.id?.endsWith(`@${MARKETPLACE}`) : row?.installed === true && row.pluginId?.endsWith(`@${MARKETPLACE}`))
177
+ .map(row => ({ id: tool === 'claude' ? row.id : row.pluginId, scope: tool === 'claude' ? row.scope ?? 'user' : 'user', cwd: row.projectPath }));
178
+ if (!plugins.some(row => row.id === PLUGIN && row.scope === 'user')) plugins.push({ id: PLUGIN, scope: 'user' });
179
+ 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.');
180
+ if (!store.official) {
181
+ if (!yes && !await confirm('Trocar a loja de plugins para a oficial? (s/n)')) {
182
+ result(`${names[tool]} — loja de plugins`, false, 'troca não autorizada; repita no terminal ou use --sim para autorizar'); continue;
183
+ }
184
+ restart = true;
185
+ if (store.exists) await run(commands[tool], ['plugin', 'marketplace', 'remove', MARKETPLACE]);
186
+ await run(commands[tool], ['plugin', 'marketplace', 'add', tool === 'claude' ? REPO : URL_GIT]);
187
+ }
188
+ restart = true;
189
+ await run(commands[tool], UPDATES[tool][0]);
190
+ ready.push(tool);
191
+ const missingApp = !rows.some(row => tool === 'claude' ? row.id === PLUGIN && row.scope === 'user' : row.pluginId === PLUGIN && row.installed);
192
+ let restored = true;
193
+ for (const row of !store.official ? plugins : missingApp ? [{ id: PLUGIN, scope: 'user' }] : []) {
194
+ try {
195
+ // O escopo de projeto usa o diretório original, sem alterar o cwd do processo.
196
+ await run({ ...commands[tool], cwd: row.cwd }, tool === 'claude' ? ['plugin', 'install', row.id, '--scope', row.scope] : ['plugin', 'add', row.id]);
197
+ } catch { restored = false; }
198
+ }
199
+ result(`${names[tool]} — loja de plugins`, restored, restored ? 'fonte oficial atualizada; plugins preservados' : `fonte oficial atualizada; falta reinstalar um plugin. ${accessHelp}`);
200
+ } catch {
201
+ result(`${names[tool]} — loja de plugins`, false, `Não foi possível conferir ou atualizar a loja. ${accessHelp}`);
202
+ }
203
+ }
204
+ if (verify) {
205
+ let latest = null, origin = '';
206
+ try {
207
+ const data = readToken();
208
+ if (data?.env === environment && expiraEm(data.token) > now()) {
209
+ const host = environment === 'dev' ? 'https://agreeable-woodpecker-190.convex.cloud' : 'https://dependable-stingray-656.convex.cloud';
210
+ 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) });
211
+ const body = await response.json();
212
+ if (response.ok && body.status === 'success') latest = body.value?.atual;
213
+ }
214
+ } catch { /* tenta loja local */ }
215
+ if (!/^\d+\.\d+\.\d+$/.test(latest ?? '')) {
216
+ 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 */ }
217
+ }
218
+ if (!/^\d+\.\d+\.\d+$/.test(latest ?? '')) latest = null;
219
+ for (const tool of ['claude', 'codex']) if (commands[tool]) {
220
+ const installed = snapshots[tool]?.version;
221
+ if (!installed) result(`${names[tool]} — chatflux-app`, 'warn', `versão não confirmada${latest && origin ? `; versão da loja: ${latest}` : ''}`);
222
+ else if (!latest) result(`${names[tool]} — chatflux-app`, 'warn', `${installed}; versão atual não confirmada`);
223
+ else if (installed !== latest) result(`${names[tool]} — chatflux-app`, false, `${installed} → atual ${latest}${origin} (vai atualizar)`);
224
+ else result(`${names[tool]} — chatflux-app`, true, `${installed}${origin}`);
225
+ }
226
+ } else {
227
+ const bridge = (tool, argv, spawnOptions) => start(commands[tool], argv, spawnOptions);
228
+ const updated = await updatePlugins({ platform, tools: ready, marketplaceUpdated: ready,
229
+ ...(platform === 'win32' ? { windowsAdapter: bridge } : { spawnImpl: (binary, argv, spawnOptions) => bridge(binary.endsWith('/claude') ? 'claude' : 'codex', argv, spawnOptions) }) });
230
+ restart ||= updated.restartRequired;
231
+ for (const tool of ['claude', 'codex']) if (commands[tool]) {
232
+ const item = updated[tool], ok = ready.includes(tool) && item.status === 'atualizado';
233
+ result(`${names[tool]} — chatflux-app`, ok, ok ? item.pluginVersion : `atualização não confirmada${item.pluginVersion ? `; versão ${item.pluginVersion}` : ''}. ${accessHelp}`);
234
+ }
235
+ }
236
+
237
+ const credential = () => { const value = readToken(); return value?.env === environment ? value : null; };
238
+ const expiry = value => value?.token ? expiraEm(value.token) : 0;
239
+ const until = value => new Date(expiry(value)).toLocaleString('pt-BR');
240
+ let loginOk = false;
241
+ try {
242
+ let data = credential();
243
+ if (expiry(data) - now() <= 15 * 86400000) {
244
+ if (verify) result('Login do portal', false, 'ausente, vencido ou vence em até 15 dias; precisa renovar no navegador');
245
+ else if (!persistent) result('Login do portal', false, 'pulado; instale o programa do portal em uma pasta permanente');
246
+ else {
247
+ await run({ binary: nodePath, prefix: [persistent] }, ['login'], true);
248
+ data = credential(); loginOk = expiry(data) > now();
249
+ result('Login do portal', loginOk, loginOk ? `conectado até ${until(data)}` : 'login não confirmado; rode gestao login');
250
+ }
251
+ } else { loginOk = true; result('Login do portal', true, `válido até ${until(data)}`); }
252
+ } catch { result('Login do portal', false, 'não foi possível conectar; rode gestao login e autorize no navegador'); }
253
+
254
+ // statusComputer protege arquivos e openJournal cria diretórios. O diagnóstico
255
+ // usa apenas um retrato dos metadados, sem ACL, journal aberto ou canal local.
256
+ function readOnlyComputer() {
257
+ const baseDir = p.join(home, '.config/gestao/computadores');
258
+ const active = json(p.join(baseDir, environment, 'ativo.json'));
259
+ if (!active?.subject) return { computerId: null, startupInstalled: false, startupHealthy: false };
260
+ const directory = p.join(baseDir, environment, p.basename(scopeDirectory({ env: environment, subject: active.subject })));
261
+ const identity = json(p.join(directory, 'identidade.json'));
262
+ const journal = json(p.join(directory, 'journal.json'));
263
+ const startupRoot = platform === 'darwin' ? p.join(home, '.config/gestao/startup') : p.join(env.LOCALAPPDATA || home, 'ChatFluxGestao/startup');
264
+ const config = json(p.join(startupRoot, environment, 'startup.json'));
265
+ const launcher = platform === 'darwin'
266
+ ? p.join(home, 'Library/LaunchAgents', `ai.chatflux.gestao-computador.${environment}.plist`)
267
+ : p.join(startupRoot, environment, 'launcher.vbs');
268
+ const configured = !!config && config.env === environment && config.platform === platform && fsImpl.existsSync(launcher) &&
269
+ [config.nodePath, config.entryPath, config.identityPath].every(file => typeof file === 'string' && !cached(file) && fsImpl.existsSync(file));
270
+ return { directory, computerId: identity?.computerId, startupInstalled: configured, startupHealthy: configured,
271
+ state: journal?.stopped ? 'desligado' : 'configurado; execução não conferida' };
272
+ }
273
+ async function checkResident(directory) {
274
+ const file = p.join(directory, 'controle.json');
275
+ let stat;
276
+ try { stat = fsImpl.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
277
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 8192) throw new Error('Controle inválido.');
278
+ const data = json(file);
279
+ 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.');
280
+ return new Promise((resolve, reject) => {
281
+ 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 => {
282
+ let body = ''; res.setEncoding('utf8');
283
+ res.on('data', chunk => { body += chunk; if (body.length > 8192) req.destroy(); });
284
+ res.on('end', () => { try { if (res.statusCode !== 200) throw new Error(); resolve(JSON.parse(body)); } catch { reject(new Error('Controle indisponível.')); } });
285
+ });
286
+ req.on('timeout', () => req.destroy()); req.on('error', reject); req.end();
287
+ });
288
+ }
289
+ try {
290
+ const status = verify ? (options.readOnlyComputer ?? readOnlyComputer)() : await (options.statusComputer ?? (await import('./computador-cli.mjs')).statusComputer)();
291
+ if (verify && status.computerId && status.startupInstalled && status.startupHealthy && status.state !== 'desligado') {
292
+ let live;
293
+ try { live = await (options.checkResident ?? checkResident)(status.directory); }
294
+ catch { live = undefined; }
295
+ if (live?.state) result('Computador', ['conectado', 'residente iniciado'].includes(live.state) ? true : 'warn', `residente em execução; ${live.state}`);
296
+ else if (live === null) result('Computador', false, 'configurado; residente não está rodando');
297
+ else result('Computador', 'warn', 'configurado; não deu para conferir se está rodando');
298
+ } else {
299
+ const connected = !!status.computerId && status.startupInstalled && status.startupHealthy &&
300
+ ['conectado', 'residente iniciado'].includes(status.state);
301
+ if (connected) result('Computador', true, verify ? status.state : 'já registrado e ligado');
302
+ else if (verify) result('Computador', false, 'precisa registrar ou ligar; rode gestao configurar');
303
+ else if (!persistent || !loginOk) result('Computador', false, 'etapa pulada; conclua a instalação e o login, depois repita gestao configurar');
304
+ else {
305
+ await run({ binary: nodePath, prefix: [persistent] }, ['ligar'], true);
306
+ result('Computador', true, 'registrado e ligado');
307
+ }
308
+ }
309
+ } catch { result('Computador', false, 'não foi possível conferir ou ligar; rode gestao ligar e siga a orientação exibida'); }
310
+ const pending = results.filter(item => item.ok !== true);
311
+ if (!pending.length) announce('\nTudo pronto neste computador ✓');
312
+ else {
313
+ announce('\nResumo:');
314
+ for (const item of pending) announce(`${item.item}: ${item.message} ${item.ok === 'warn' ? '⚠' : '✗'}`);
315
+ }
316
+ if (restart) announce('Para terminar: reinicie o VS Code e abra uma nova conversa no Claude Code/Codex.');
317
+ if (verify && pending.length) announce('Para resolver, rode: npx -y chatflux-gestao-agente@latest configurar');
318
+ return { results, restartRequired: restart };
319
+ }
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.3",
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": [