@fazer-ai/agents 1.0.0

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/agents/claude.js +152 -0
  4. package/dist/agents/codex.js +155 -0
  5. package/dist/agents/detect.js +15 -0
  6. package/dist/agents/handoff.js +22 -0
  7. package/dist/agents/hermes-skills.js +177 -0
  8. package/dist/agents/hermes.js +474 -0
  9. package/dist/agents/index.js +57 -0
  10. package/dist/agents/manual.js +22 -0
  11. package/dist/agents/other.js +39 -0
  12. package/dist/agents/shell.js +15 -0
  13. package/dist/agents/types.js +2 -0
  14. package/dist/config.js +48 -0
  15. package/dist/exec.js +279 -0
  16. package/dist/hostinger.js +75 -0
  17. package/dist/hub-command.js +144 -0
  18. package/dist/index.js +726 -0
  19. package/dist/licenses.js +93 -0
  20. package/dist/mcp.js +100 -0
  21. package/dist/oauth.js +578 -0
  22. package/dist/onboarding-marker.js +48 -0
  23. package/dist/preferences.js +40 -0
  24. package/dist/skills/agents-dev/SKILL.md +37 -0
  25. package/dist/skills/agents-dev/gotchas.md +6 -0
  26. package/dist/skills/agents-dev/guardrails.md +6 -0
  27. package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
  28. package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
  29. package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
  30. package/dist/skills/agents-dev/references/03-implement.md +9 -0
  31. package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
  32. package/dist/skills/agents-onboarding/SKILL.md +80 -0
  33. package/dist/skills/agents-onboarding/gotchas.md +157 -0
  34. package/dist/skills/agents-onboarding/guardrails.md +65 -0
  35. package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
  36. package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
  37. package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
  38. package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
  39. package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
  40. package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
  41. package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
  42. package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
  43. package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
  44. package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
  45. package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
  46. package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
  47. package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
  48. package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
  49. package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
  50. package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
  51. package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
  52. package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
  53. package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
  54. package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
  55. package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
  56. package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
  57. package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
  58. package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
  59. package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
  60. package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
  61. package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
  62. package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
  63. package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
  64. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
  65. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
  66. package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
  67. package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
  68. package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
  69. package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
  70. package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
  71. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
  72. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
  73. package/dist/skills/agents-operation/SKILL.md +42 -0
  74. package/dist/skills/agents-operation/gotchas.md +61 -0
  75. package/dist/skills/agents-operation/guardrails.md +26 -0
  76. package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
  77. package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
  78. package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
  79. package/dist/skills/agents-operation/references/03-adjust.md +36 -0
  80. package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
  81. package/dist/ui-select.js +279 -0
  82. package/dist/ui.js +167 -0
  83. package/package.json +53 -0
package/dist/index.js ADDED
@@ -0,0 +1,726 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.parseArgs = parseArgs;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const agents_1 = require("./agents");
8
+ const hermes_skills_1 = require("./agents/hermes-skills");
9
+ const config_1 = require("./config");
10
+ const exec_1 = require("./exec");
11
+ const hostinger_1 = require("./hostinger");
12
+ const hub_command_1 = require("./hub-command");
13
+ const licenses_1 = require("./licenses");
14
+ const onboarding_marker_1 = require("./onboarding-marker");
15
+ const oauth_1 = require("./oauth");
16
+ const preferences_1 = require("./preferences");
17
+ const ui_1 = require("./ui");
18
+ const ui_select_1 = require("./ui-select");
19
+ // NOTE: versão = a do package.json (fonte única). __dirname é dist/ (build) ou src/ (dev); em ambos
20
+ // ../package.json é a raiz do pacote, inclusive instalado via npm (package.json sempre no tarball).
21
+ const { version: CLI_VERSION } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "../package.json"), "utf8"));
22
+ function parseArgs(argv) {
23
+ const out = {
24
+ help: false,
25
+ version: false,
26
+ dryRun: false,
27
+ yes: false,
28
+ noHandoff: false,
29
+ login: false,
30
+ verbose: false,
31
+ };
32
+ const positional = [];
33
+ let parsingOptions = true;
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const arg = argv[i];
36
+ if (parsingOptions && arg === "--") {
37
+ parsingOptions = false;
38
+ }
39
+ else if (parsingOptions && (arg === "--help" || arg === "-h")) {
40
+ out.help = true;
41
+ }
42
+ else if (parsingOptions && (arg === "--version" || arg === "-V")) {
43
+ out.version = true;
44
+ }
45
+ else if (parsingOptions && (arg === "--dry-run" || arg === "--dryrun")) {
46
+ out.dryRun = true;
47
+ }
48
+ else if (parsingOptions && (arg === "--yes" || arg === "-y")) {
49
+ out.yes = true;
50
+ }
51
+ else if (parsingOptions && arg === "--no-handoff") {
52
+ out.noHandoff = true;
53
+ }
54
+ else if (parsingOptions && arg === "--login") {
55
+ out.login = true;
56
+ }
57
+ else if (parsingOptions && (arg === "--verbose" || arg === "-v")) {
58
+ out.verbose = true;
59
+ }
60
+ else if (parsingOptions && arg === "--pro") {
61
+ out.pro = true;
62
+ }
63
+ else if (parsingOptions && arg === "--free") {
64
+ out.pro = false;
65
+ }
66
+ else if (parsingOptions && arg.startsWith("--agent=")) {
67
+ out.agent = arg.slice("--agent=".length);
68
+ }
69
+ else if (parsingOptions && arg === "--agent") {
70
+ const next = argv[i + 1];
71
+ if (next === undefined)
72
+ throw new Error("--agent requer um valor");
73
+ out.agent = next;
74
+ i++;
75
+ }
76
+ else if (parsingOptions && arg.startsWith("--provider=")) {
77
+ out.provider = arg.slice("--provider=".length);
78
+ }
79
+ else if (parsingOptions && arg === "--provider") {
80
+ const next = argv[i + 1];
81
+ if (next === undefined)
82
+ throw new Error("--provider requer um valor");
83
+ out.provider = next;
84
+ i++;
85
+ }
86
+ else if (parsingOptions && arg.startsWith("-") && arg !== "-") {
87
+ throw new Error(`opção desconhecida: ${arg}`);
88
+ }
89
+ else {
90
+ positional.push(arg);
91
+ }
92
+ }
93
+ if (positional.length > 0) {
94
+ throw new Error("argumentos extras inesperados");
95
+ }
96
+ return out;
97
+ }
98
+ function printHelp() {
99
+ process.stderr.write((0, ui_1.banner)());
100
+ process.stderr.write(`Uso: bunx @fazer-ai/agents@latest [opções]
101
+ npx @fazer-ai/agents@latest [opções]
102
+
103
+ Prepara o seu agente de IA para o onboarding da fazer.ai agents: instala o agente
104
+ se faltar, faz login na fazer.ai, conecta as ferramentas,
105
+ instala o guia de onboarding e abre o agente.
106
+
107
+ Opções:
108
+ --agent <id> Agente: claude, codex, hermes ou other. Sem isto, o CLI pergunta.
109
+ --provider <id> Provider de VPS/DNS: hostinger (default) ou other.
110
+ --login Força um novo login no browser (ignora a sessão salva).
111
+ --pro Edição Pro da fazer.ai agents (requer membership da comunidade).
112
+ --free Edição Free da fazer.ai agents (pula a pergunta de edição).
113
+ --dry-run Só mostra o que faria, sem executar.
114
+ -y, --yes Executa sem confirmação interativa.
115
+ --no-handoff Prepara tudo, mas não abre o agente ao final.
116
+ -v, --verbose Mostra a saída dos comandos (normalmente silenciada) + diagnósticos.
117
+ -V, --version Mostra a versão do CLI.
118
+ -h, --help Mostra esta ajuda.
119
+
120
+ Ambiente:
121
+ FAZER_AI_HUB_URL Sobrescreve a base do hub (default: https://app.fazer.ai).
122
+ FAZER_AI_CLI_THEME Tema do CLI: light para fundo claro (default: escuro).
123
+ HOSTINGER_API_TOKEN Token da API Hostinger (para conectar as ferramentas).
124
+ AGENTS_OAUTH_CLIENT_ID client_id OAuth pré-registrado (se o hub não usar DCR).
125
+
126
+ Por padrão o CLI faz login OAuth no browser e reaproveita a sessão nas próximas
127
+ vezes (cache em ~/.fazer-ai/oauth.json). Use --login pra forçar um login novo.
128
+
129
+ A última escolha de agente e provider fica salva (~/.fazer-ai/preferences.json) e
130
+ vira o default ("último usado") no próximo run; os flags --agent/--provider têm
131
+ prioridade.
132
+ `);
133
+ }
134
+ // Prévia curta (só dry-run): o que cada etapa faz, em palavras simples. Sem
135
+ // comandos nem detalhes técnicos.
136
+ function renderPreview(adapter, steps) {
137
+ process.stderr.write(`\n${(0, ui_1.headline)(`O que vou fazer · ${adapter.displayName}`)}\n`);
138
+ steps.forEach((s, idx) => {
139
+ process.stderr.write(` ${ui_1.c.cyan(`${idx + 1}.`)} ${s.title}\n`);
140
+ });
141
+ }
142
+ async function main() {
143
+ const rawArgs = process.argv.slice(2);
144
+ // Proxy do hub (`agents hub <op>`): o agente de onboarding chama as ops do hub por aqui, não pelo
145
+ // hub MCP. Curto-circuita antes do banner/fluxo de onboarding; usa a sessão OAuth já no oauth.json.
146
+ if (rawArgs[0] === "hub") {
147
+ await (0, hub_command_1.runHubCommand)((0, config_1.resolveConfig)(), rawArgs.slice(1), (m) => process.stderr.write(`${m}\n`));
148
+ return;
149
+ }
150
+ const args = parseArgs(rawArgs);
151
+ if (args.help) {
152
+ printHelp();
153
+ return;
154
+ }
155
+ if (args.version) {
156
+ process.stdout.write(`${CLI_VERSION}\n`);
157
+ return;
158
+ }
159
+ // Instaladores nativos (Claude/Hermes) gravam em ~/.local/bin sem pô-lo no PATH
160
+ // persistente; garante-o na sessão pra detectar o agente (recém-instalado ou de um run
161
+ // anterior) sem depender de reabrir o terminal.
162
+ (0, exec_1.ensureUserLocalBinOnPath)();
163
+ // E persiste ~/.local/bin no User PATH do Windows: a etapa de OAuth do MCP pede pra REABRIR o
164
+ // agente num terminal NOVO, que só acha o binário se o dir estiver no PATH persistente.
165
+ await (0, exec_1.persistUserLocalBinToWindowsPath)();
166
+ // Começa numa tela limpa (só em TTY, respeitando NO_COLOR): tira o ruído do bunx/npx
167
+ // acima do banner e dá uma primeira impressão limpa.
168
+ (0, ui_1.clearScreen)();
169
+ process.stderr.write((0, ui_1.banner)());
170
+ const config = (0, config_1.resolveConfig)();
171
+ // Surface os overrides de env load-bearing pra nada apontar pra outro lugar
172
+ // (hub/marketplace/plugin/client) em silêncio.
173
+ const overrides = [];
174
+ if (process.env.FAZER_AI_HUB_URL?.trim())
175
+ overrides.push(`hub=${config.hubBaseUrl}`);
176
+ if (process.env.AGENTS_MARKETPLACE_TARGET?.trim())
177
+ overrides.push(`marketplace=${config.marketplaceAddTarget}`);
178
+ if (process.env.AGENTS_MARKETPLACE_NAME?.trim())
179
+ overrides.push(`marketplaceName=${config.marketplaceName}`);
180
+ if (process.env.AGENTS_PLUGIN?.trim())
181
+ overrides.push(`plugin=${config.onboardingPlugin}`);
182
+ if (process.env.AGENTS_OAUTH_CLIENT_ID?.trim())
183
+ overrides.push("oauthClientId=set");
184
+ if (overrides.length) {
185
+ process.stderr.write(`${ui_1.sym.warn} Overrides: ${overrides.join(", ")}.\n`);
186
+ }
187
+ // Verbose: diagnósticos de ambiente (versão do CLI, runtime, plataforma, se as skills
188
+ // vêm empacotadas). O resto do verbose (saída dos comandos silenciados) é passado
189
+ // adiante em ExecContext.verbose.
190
+ if (args.verbose) {
191
+ const runtime = process.versions.bun
192
+ ? `bun ${process.versions.bun}`
193
+ : `node ${process.versions.node}`;
194
+ const bundle = (0, hermes_skills_1.bundledSkillsRoot)() ? "empacotadas" : "não empacotadas (rede)";
195
+ process.stderr.write(`${(0, ui_1.detail)(`CLI ${CLI_VERSION} · ${runtime} · ${process.platform}/${process.arch} · skills ${bundle}`)}\n`);
196
+ }
197
+ let selected = args.agent ? (0, agents_1.getAgent)(args.agent) : undefined;
198
+ if (args.agent && !selected) {
199
+ throw new Error(`agente não suportado: ${args.agent} (suportados: ${agents_1.AGENTS.map((a) => a.id).join(", ")})`);
200
+ }
201
+ const prefs = (0, preferences_1.loadPreferences)();
202
+ const detected = await (0, agents_1.detectAgents)();
203
+ process.stderr.write(`${(0, ui_1.phaseHeader)(1, 4, "Agente")}\n`);
204
+ if (!selected) {
205
+ // Sem --agent: menu interativo (TTY) ou auto-pick (sem TTY, ex.: CI/pipe).
206
+ // O default vem da última seleção salva (se ainda válida).
207
+ const preferred = prefs.agent
208
+ ? detected.find((d) => d.adapter.id === prefs.agent)?.adapter
209
+ : undefined;
210
+ selected = process.stdin.isTTY
211
+ ? await selectAgent(detected, prefs.agent)
212
+ : (preferred ??
213
+ detected.find((d) => d.result.installed)?.adapter ??
214
+ detected[0]?.adapter ??
215
+ agents_1.AGENTS[0]);
216
+ }
217
+ if (!selected)
218
+ throw new Error("nenhum adapter de agente registrado");
219
+ const adapter = selected;
220
+ let detection = detected.find((d) => d.adapter.id === adapter.id)?.result ??
221
+ (await adapter.detect());
222
+ const installCmd = (0, agents_1.installCommandFor)(adapter.installCommand);
223
+ // O agente não está instalado, mas o próximo passo o instala (automatizado +
224
+ // há instalador pra esta plataforma): tom neutro, não um alerta de erro.
225
+ const willInstall = adapter.automated && !detection.installed && Boolean(installCmd);
226
+ process.stderr.write(`${(0, ui_1.targetLine)(adapter.displayName, {
227
+ installed: detection.installed,
228
+ version: detection.version,
229
+ hasDetect: adapter.detectCommand !== "",
230
+ willInstall,
231
+ })}\n`);
232
+ // O aviso "fora do PATH" só faz sentido quando NÃO há instalação a seguir
233
+ // (com willInstall, o passo seguinte resolve, o alerta seria ruído).
234
+ if (adapter.detectCommand !== "" && !detection.installed && !willInstall) {
235
+ process.stderr.write(`${(0, ui_1.detail)(`\`${adapter.detectCommand}\` falhou; pode não estar no PATH.`)}\n`);
236
+ }
237
+ process.stderr.write(`${(0, ui_1.phaseHeader)(2, 4, "Provedor de infraestrutura")}\n`);
238
+ const provider = await resolveProvider(args, prefs.provider);
239
+ process.stderr.write(`${ui_1.sym.ok} Infra: ${provider === "hostinger" ? "Hostinger" : "Outro provider"}.\n`);
240
+ const ctx = {
241
+ config,
242
+ provider,
243
+ hostingerTokenProvided: Boolean(process.env.HOSTINGER_API_TOKEN),
244
+ };
245
+ if (args.dryRun) {
246
+ if (willInstall && installCmd) {
247
+ process.stderr.write(`\n${ui_1.sym.info} ${adapter.displayName} não está instalado; será instalado antes:\n`);
248
+ process.stderr.write(`${(0, ui_1.detail)(installCmd)}\n`);
249
+ }
250
+ renderPreview(adapter, adapter.plan(ctx));
251
+ process.stderr.write(`\n${ui_1.sym.info} ${ui_1.c.dim("nada foi executado (dry-run).")}\n`);
252
+ if (!ctx.hostingerTokenProvided && provider === "hostinger") {
253
+ process.stderr.write(`${(0, ui_1.detail)("o CLI vai pedir o token da Hostinger na execução (ou defina HOSTINGER_API_TOKEN antes).")}\n`);
254
+ }
255
+ return;
256
+ }
257
+ // Run real (passou do dry-run): lembra a escolha de agente + provider para
258
+ // virar o default no próximo run (qualquer origem: flag, picker, auto-pick).
259
+ (0, preferences_1.savePreferences)({ agent: adapter.id, provider });
260
+ // Agente automatizado ausente: instala (com confirmação) e re-detecta.
261
+ if (adapter.automated && !detection.installed) {
262
+ detection = await ensureInstalled(adapter, detection, args.yes);
263
+ }
264
+ // Auth / momentary-login: agentes com fluxo de login no CLI (Codex/Hermes). O Claude
265
+ // não entra aqui (sem adapter.login): autentica no próprio TUI ao abrir o handoff, e
266
+ // um pre-login duplicaria o sign-in.
267
+ if (detection.installed && adapter.login) {
268
+ await ensureLoggedIn(adapter);
269
+ }
270
+ // Adapter manual (só "other"): execute() apenas imprime o plano + o prompt, não
271
+ // muta a máquina. Hermes/Claude/Codex são automatizados (seguem abaixo).
272
+ if (!adapter.automated) {
273
+ await adapter.execute({
274
+ config,
275
+ provider,
276
+ hostingerToken: process.env.HOSTINGER_API_TOKEN,
277
+ hostingerTokenProvided: Boolean(process.env.HOSTINGER_API_TOKEN),
278
+ handoff: !args.noHandoff,
279
+ verbose: args.verbose,
280
+ log: (message) => process.stderr.write(`${message}\n`),
281
+ });
282
+ return;
283
+ }
284
+ // O onboarding lista as licenças reais do hub (login OAuth no browser) e abre o
285
+ // agente; precisa de um terminal interativo.
286
+ if (!process.stdin.isTTY) {
287
+ throw new Error("o onboarding precisa de um terminal interativo para o login na fazer.ai.");
288
+ }
289
+ process.stderr.write(`${(0, ui_1.phaseHeader)(3, 4, "Chatwoot")}\n`);
290
+ // Chatwoot: subir um novo ou plugar num já existente. O login OAuth (list_licenses do
291
+ // hub) acontece aqui de todo jeito: mesmo no "existing", as licenças alimentam o gate da
292
+ // edição do fazer.ai agents (community access); o --login força uma sessão nova.
293
+ const chatwootSource = await selectChatwootSource(prefs);
294
+ let choice;
295
+ let licenses;
296
+ if (chatwootSource === "new") {
297
+ ({ choice, licenses } = await selectChatwootTier(config, prefs, args.login));
298
+ }
299
+ else {
300
+ // Chatwoot existente: sem seleção de licença; só lista as licenças pro gate da edição.
301
+ const log = (message) => process.stderr.write(`${message}\n`);
302
+ try {
303
+ licenses = await (0, oauth_1.listHubLicenses)(config, { log, forceLogin: args.login });
304
+ }
305
+ catch (error) {
306
+ const msg = error instanceof Error ? error.message : String(error);
307
+ log(`${ui_1.sym.warn} Não consegui listar as licenças do hub (${msg}).`);
308
+ licenses = [];
309
+ }
310
+ choice = { chatwootSource: "existing" };
311
+ }
312
+ const edition = await selectEdition(licenses, args);
313
+ const marker = { ...choice, edition };
314
+ (0, onboarding_marker_1.saveOnboardingMarker)(marker);
315
+ (0, preferences_1.savePreferences)({ chatwootSource });
316
+ if (choice.chatwootTier === "pro" && choice.chatwootLicenseId) {
317
+ (0, preferences_1.savePreferences)({ chatwootLicenseId: choice.chatwootLicenseId });
318
+ }
319
+ process.stderr.write(`${ui_1.sym.ok} Chatwoot: ${choice.chatwootSource === "existing"
320
+ ? "existente (conecta ao seu)"
321
+ : choice.chatwootTier === "pro"
322
+ ? `Pro (Kanban), licença ${choice.chatwootLicenseId}`
323
+ : "OSS (sem Kanban)"}.\n`);
324
+ process.stderr.write(`${ui_1.sym.ok} fazer.ai agents: ${edition === "pro" ? "Pro (multi-tenant)" : "Free (1 tenant)"}.\n`);
325
+ // Hostinger precisa do token pra conectar os MCPs; captura se faltar.
326
+ if (provider === "hostinger")
327
+ await resolveHostingerToken();
328
+ process.stderr.write(`${(0, ui_1.phaseHeader)(4, 4, "Configurar o agente")}\n`);
329
+ await adapter.execute({
330
+ config,
331
+ provider,
332
+ hostingerToken: process.env.HOSTINGER_API_TOKEN,
333
+ hostingerTokenProvided: Boolean(process.env.HOSTINGER_API_TOKEN),
334
+ handoff: !args.noHandoff,
335
+ verbose: args.verbose,
336
+ log: (message) => process.stderr.write(`${message}\n`),
337
+ });
338
+ process.stderr.write(`\n${ui_1.sym.ok} ${ui_1.c.bold("Pronto.")}\n`);
339
+ }
340
+ // Lê um segredo do TTY sem ecoar (mostra `*`). Raw mode, sem dependências.
341
+ function askSecret(question) {
342
+ if (!process.stdin.isTTY)
343
+ return Promise.resolve("");
344
+ process.stderr.write(question);
345
+ const stdin = process.stdin;
346
+ return new Promise((resolve) => {
347
+ let buf = "";
348
+ stdin.setRawMode(true);
349
+ stdin.resume();
350
+ stdin.setEncoding("utf8");
351
+ const onData = (chunk) => {
352
+ for (const ch of chunk) {
353
+ if (ch === "\r" || ch === "\n" || ch === "\u0004") {
354
+ stdin.setRawMode(false);
355
+ stdin.pause();
356
+ stdin.removeListener("data", onData);
357
+ process.stderr.write("\n");
358
+ resolve(buf);
359
+ return;
360
+ }
361
+ if (ch === "\u0003") {
362
+ stdin.setRawMode(false);
363
+ process.stderr.write("\n");
364
+ process.exit(130);
365
+ }
366
+ if (ch === "\u007f" || ch === "\b") {
367
+ if (buf.length > 0) {
368
+ buf = buf.slice(0, -1);
369
+ process.stderr.write("\b \b");
370
+ }
371
+ continue;
372
+ }
373
+ buf += ch;
374
+ process.stderr.write("*");
375
+ }
376
+ };
377
+ stdin.on("data", onData);
378
+ });
379
+ }
380
+ // Garante o token da Hostinger no ambiente (env → cache → prompt mascarado).
381
+ // Setá-lo em process.env faz o env-ref ${HOSTINGER_API_TOKEN} resolver no
382
+ // handoff sem virar literal em argv. Sem token, os MCPs da Hostinger são pulados.
383
+ async function resolveHostingerToken() {
384
+ // Env explícito: usa; valida só pra avisar (a escolha do usuário tem precedência).
385
+ const envToken = process.env.HOSTINGER_API_TOKEN?.trim();
386
+ if (envToken) {
387
+ if ((await (0, hostinger_1.validateHostingerToken)(envToken)) === false) {
388
+ process.stderr.write(`${ui_1.sym.warn} O HOSTINGER_API_TOKEN definido foi recusado pela Hostinger (401). Vou usá-lo mesmo assim; se as ferramentas falharem, gere um novo.\n`);
389
+ }
390
+ return;
391
+ }
392
+ // Cache: reusa só se ainda válido (pode ter expirado/sido revogado entre sessões).
393
+ const cached = (0, hostinger_1.loadHostingerToken)();
394
+ if (cached) {
395
+ const ok = await (0, hostinger_1.validateHostingerToken)(cached);
396
+ if (ok === true) {
397
+ process.env.HOSTINGER_API_TOKEN = cached;
398
+ process.stderr.write(`${ui_1.sym.ok} Token da Hostinger reaproveitado (~/.fazer-ai/hostinger.json).\n`);
399
+ return;
400
+ }
401
+ if (ok === "error") {
402
+ // Não deu pra validar (sem conexão): usa o cache mesmo (best-effort).
403
+ process.env.HOSTINGER_API_TOKEN = cached;
404
+ process.stderr.write(`${ui_1.sym.ok} Token da Hostinger reaproveitado (não validado: sem conexão com a API).\n`);
405
+ return;
406
+ }
407
+ // ok === false: o token salvo morreu; cai no prompt por um novo.
408
+ process.stderr.write(`${ui_1.sym.warn} O token salvo expirou ou foi revogado; vou pedir um novo.\n`);
409
+ }
410
+ if (!process.stdin.isTTY)
411
+ return;
412
+ process.stderr.write(`\n${(0, ui_1.headline)("Token da API Hostinger")}\n`);
413
+ const hpanelApiUrl = "https://hpanel.hostinger.com/api";
414
+ process.stderr.write(`${(0, ui_1.detail)(`Abrindo o hPanel em ${hpanelApiUrl} pra gerar a chave (ou acesse o link manualmente). Enter em branco pula as ferramentas de infra.`)}\n`);
415
+ (0, oauth_1.openBrowser)(hpanelApiUrl);
416
+ // Loop: pede, valida e repete enquanto a Hostinger recusar (401).
417
+ for (;;) {
418
+ const token = (await askSecret(`${ui_1.sym.info} Cole o token: `)).trim();
419
+ if (!token) {
420
+ process.stderr.write(`${ui_1.sym.warn} Sem token; vou pular as ferramentas da Hostinger. Defina HOSTINGER_API_TOKEN e rode de novo pra conectá-las.\n`);
421
+ return;
422
+ }
423
+ const ok = await (0, hostinger_1.validateHostingerToken)(token);
424
+ if (ok === false) {
425
+ process.stderr.write(`${ui_1.sym.err} Token recusado pela Hostinger (401). Confira ou gere outro em ${hpanelApiUrl} e cole de novo.\n`);
426
+ continue;
427
+ }
428
+ process.env.HOSTINGER_API_TOKEN = token;
429
+ (0, hostinger_1.saveHostingerToken)(token);
430
+ process.stderr.write(ok === true
431
+ ? `${ui_1.sym.ok} Token validado e salvo em ~/.fazer-ai/hostinger.json (0600).\n`
432
+ : `${ui_1.sym.ok} Token salvo em ~/.fazer-ai/hostinger.json (0600). Não consegui validar agora (sem conexão); se as ferramentas falharem, gere um novo.\n`);
433
+ return;
434
+ }
435
+ }
436
+ // Agente recomendado no picker (o Instalador fazer.ai / Hermes): é o mais integrado
437
+ // à jornada e o que abre já no onboarding. Carrega o selo "recomendado" e é o default
438
+ // quando não há uma última escolha salva.
439
+ const RECOMMENDED_AGENT_ID = "hermes";
440
+ // Sem --agent (e com TTY): menu para o usuário escolher o agente, com status de detecção
441
+ // e um selo "recomendado" no Instalador fazer.ai. Enter usa o padrão: a última escolha
442
+ // salva (se ainda válida), senão o recomendado.
443
+ async function selectAgent(detected, prefAgentId) {
444
+ const fallback = detected[0]?.adapter ?? agents_1.AGENTS[0];
445
+ if (!fallback)
446
+ throw new Error("nenhum adapter de agente registrado");
447
+ // Default: última seleção salva (se ainda válida) → senão o recomendado → senão o
448
+ // primeiro instalado → senão o primeiro da lista.
449
+ const preferred = prefAgentId
450
+ ? detected.find((d) => d.adapter.id === prefAgentId)?.adapter
451
+ : undefined;
452
+ const recommended = detected.find((d) => d.adapter.id === RECOMMENDED_AGENT_ID)?.adapter;
453
+ const def = preferred ??
454
+ recommended ??
455
+ detected.find((d) => d.result.installed)?.adapter ??
456
+ fallback;
457
+ const defIdx = Math.max(0, detected.findIndex((d) => d.adapter === def));
458
+ const options = detected.map((d, i) => {
459
+ let status = "";
460
+ if (d.adapter.detectCommand !== "") {
461
+ status = d.result.installed
462
+ ? ui_1.c.green(d.result.version ?? "instalado")
463
+ : ui_1.c.gray("não instalado");
464
+ }
465
+ const badge = d.adapter.id === RECOMMENDED_AGENT_ID ? ui_1.c.cyan("recomendado") : "";
466
+ const tag = i === defIdx ? ui_1.c.gray(preferred ? "(último usado)" : "(padrão)") : "";
467
+ const hint = [badge, status, tag].filter(Boolean).join(" ");
468
+ return { value: d.adapter, label: d.adapter.displayName, hint };
469
+ });
470
+ return (0, ui_select_1.select)({
471
+ title: "Qual agente você prefere usar? Recomendamos o Instalador fazer.ai.",
472
+ options,
473
+ defaultIndex: defIdx,
474
+ });
475
+ }
476
+ // Provider de infra: do flag/env, senão um picker (TTY), senão o default.
477
+ // O default vem da última seleção salva (se válida), senão hostinger.
478
+ async function resolveProvider(args, prefProvider) {
479
+ const explicit = args.provider ?? process.env.AGENTS_PROVIDER;
480
+ if (explicit)
481
+ return (0, config_1.asProvider)(explicit);
482
+ const fromPref = prefProvider === "hostinger" || prefProvider === "other";
483
+ const def = prefProvider === "other" ? "other" : "hostinger";
484
+ if (!process.stdin.isTTY)
485
+ return def;
486
+ return selectProvider(def, fromPref);
487
+ }
488
+ // Sem --provider (e com TTY): pergunta onde está a infra. Enter = default.
489
+ async function selectProvider(def = "hostinger", fromPref = false) {
490
+ const choices = [
491
+ {
492
+ id: "hostinger",
493
+ label: "Hostinger: DNS, VPS e domínio automatizados",
494
+ extra: ui_1.c.cyan("recomendado"),
495
+ },
496
+ { id: "other", label: "Outro provider" },
497
+ ];
498
+ const defIdx = Math.max(0, choices.findIndex((o) => o.id === def));
499
+ const options = choices.map((o, i) => {
500
+ const tag = i === defIdx ? ui_1.c.gray(fromPref ? "(último usado)" : "(padrão)") : "";
501
+ return {
502
+ value: o.id,
503
+ label: o.label,
504
+ hint: [o.extra ?? "", tag].filter(Boolean).join(" "),
505
+ };
506
+ });
507
+ // Dica pra quem ainda não tem infra: a Hostinger é a rota automatizada.
508
+ process.stderr.write(`${(0, ui_1.detail)("Não tem VPS ainda? Recomendamos a Hostinger: o CLI provisiona DNS/VPS/domínio por você.")}\n`);
509
+ return (0, ui_select_1.select)({
510
+ title: "Onde está a sua infraestrutura (VPS/DNS)?",
511
+ options,
512
+ defaultIndex: defIdx,
513
+ });
514
+ }
515
+ // Chatwoot: subir um novo (cai na seleção de licença/tier) ou usar um já existente
516
+ // (BYO: o fazer.ai agents só pluga, sem provisionar Chatwoot nem escolher licença). Enter usa o
517
+ // default: a última escolha salva (se houver), senão "new". Só com TTY (o fluxo já garante).
518
+ async function selectChatwootSource(prefs) {
519
+ const fromPref = prefs.chatwootSource === "new" || prefs.chatwootSource === "existing";
520
+ const def = prefs.chatwootSource === "existing" ? "existing" : "new";
521
+ const choices = [
522
+ {
523
+ id: "new",
524
+ label: "Subir um Chatwoot novo",
525
+ hint: "escolhe a edição/licença a seguir",
526
+ },
527
+ {
528
+ id: "existing",
529
+ label: "Usar um Chatwoot que já existe",
530
+ hint: "conecta o fazer.ai agents ao seu Chatwoot já existente",
531
+ },
532
+ ];
533
+ const defIdx = Math.max(0, choices.findIndex((o) => o.id === def));
534
+ const options = choices.map((o, i) => {
535
+ const tag = i === defIdx ? ui_1.c.gray(fromPref ? "(último usado)" : "(padrão)") : "";
536
+ return { value: o.id, label: o.label, hint: [o.hint, tag].filter(Boolean).join(" ") };
537
+ });
538
+ return (0, ui_select_1.select)({
539
+ title: "Chatwoot: subir um novo ou usar um já existente?",
540
+ options,
541
+ defaultIndex: defIdx,
542
+ });
543
+ }
544
+ // Seleção da edição do Chatwoot via licença do Kanban (Chatwoot Pro). Lista as
545
+ // licenças reais do hub (list_licenses), com copy ciente do estado: licença livre
546
+ // → Pro; licença atribuída → alerta pra desvincular no hub + recarrega; sem
547
+ // nenhuma disponível → "Adquirir" (carrinho) ou "Seguir sem"; lista vazia →
548
+ // "Atualizar" ou "Ainda não tenho licença" (porta da comunidade do Lucas
549
+ // Moreira). Roda só com TTY + sessão do hub. Devolve a escolha + as licenças do
550
+ // último snapshot (reusadas pra decidir a edição do fazer.ai agents, sem re-listar).
551
+ async function selectChatwootTier(config, prefs, forceLogin) {
552
+ const log = (message) => process.stderr.write(`${message}\n`);
553
+ const app = config.hubBaseUrl;
554
+ // O --login força um login novo só na 1ª listagem; os refreshes reusam a sessão.
555
+ let force = forceLogin;
556
+ for (;;) {
557
+ let licenses = [];
558
+ try {
559
+ licenses = await (0, oauth_1.listHubLicenses)(config, { log, forceLogin: force });
560
+ force = false;
561
+ }
562
+ catch (error) {
563
+ const msg = error instanceof Error ? error.message : String(error);
564
+ log(`${ui_1.sym.warn} Não consegui listar as licenças do hub (${msg}).`);
565
+ }
566
+ const kanban = (0, licenses_1.kanbanLicenses)(licenses);
567
+ const { options, defaultIndex, state } = (0, licenses_1.buildLicenseMenu)(kanban, prefs.chatwootLicenseId);
568
+ const title = state === "empty"
569
+ ? "Licença do Kanban: nenhuma licença encontrada"
570
+ : "Licença do Kanban (Chatwoot Pro)";
571
+ const choice = (0, licenses_1.parseLicenseChoice)(await (0, ui_select_1.select)({ title, options, defaultIndex }));
572
+ if (choice.kind === "available") {
573
+ return {
574
+ choice: {
575
+ chatwootSource: "new",
576
+ chatwootTier: "pro",
577
+ chatwootLicenseId: choice.id,
578
+ },
579
+ licenses,
580
+ };
581
+ }
582
+ // Licença já atribuída: alerta pra desvincular no hub; o "OK" recarrega.
583
+ if (choice.kind === "attached") {
584
+ process.stderr.write(`\n${ui_1.sym.warn} Essa licença já está atribuída a uma instância. Desvincule-a em ${app} (a instância → desvincular licença).\n`);
585
+ (0, oauth_1.openBrowser)(app);
586
+ await (0, ui_select_1.acknowledge)("Pressione Enter para atualizar a lista.");
587
+ continue;
588
+ }
589
+ if (choice.action === "refresh")
590
+ continue;
591
+ // Adquirir: abre o carrinho do hub; o "OK" recarrega (a nova licença aparece).
592
+ if (choice.action === "acquire") {
593
+ const cart = `${app}?cart=1`;
594
+ process.stderr.write(`\n${(0, ui_1.detail)(`Abrindo o carrinho em ${cart} (ou acesse o link manualmente).`)}\n`);
595
+ (0, oauth_1.openBrowser)(cart);
596
+ await (0, ui_select_1.acknowledge)("Pressione Enter para atualizar a lista.");
597
+ continue;
598
+ }
599
+ // "Ainda não tenho licença" (só no estado vazio): oferta da comunidade.
600
+ if (choice.action === "community") {
601
+ process.stderr.write(`\n${(0, ui_1.headline)("Licença grátis do Kanban pela comunidade")}\n`);
602
+ process.stderr.write(`${(0, ui_1.detail)("Vire membro Pro da comunidade do Lucas Moreira (lucasmoreira.ai): licença grátis do Kanban (1 conta no plano mensal, ou 2 ilimitadas no anual).")}\n`);
603
+ process.stderr.write(`${(0, ui_1.detail)("Abrindo lucasmoreira.ai no navegador (ou acesse o link manualmente).")}\n`);
604
+ (0, oauth_1.openBrowser)("https://lucasmoreira.ai");
605
+ const next = await (0, ui_select_1.select)({
606
+ title: "E agora?",
607
+ options: [
608
+ { value: "member", label: "Já me tornei membro (recarregar licenças)" },
609
+ { value: "skip", label: "Seguir sem o Chatwoot Pro (OSS, sem Kanban)" },
610
+ ],
611
+ });
612
+ if (next === "member")
613
+ continue;
614
+ return {
615
+ choice: { chatwootSource: "new", chatwootTier: "community" },
616
+ licenses,
617
+ };
618
+ }
619
+ // "Seguir sem o Chatwoot Pro" (action === "skip").
620
+ return {
621
+ choice: { chatwootSource: "new", chatwootTier: "community" },
622
+ licenses,
623
+ };
624
+ }
625
+ }
626
+ // Seleção da edição da fazer.ai agents (app), reusando as licenças já listadas no
627
+ // passo do Chatwoot (sem re-listar). `--pro` força Pro (erra se a conta não tem
628
+ // acesso e não há como perguntar); `--free` força Free; sem flag, pergunta sempre
629
+ // (upsell). Acesso Pro = membership da comunidade (hasCommunityAccess); sem ela, o
630
+ // projeto `agents` no Harbor não abre, então segue Free com instrução.
631
+ async function selectEdition(licenses, args) {
632
+ const access = (0, licenses_1.hasCommunityAccess)(licenses);
633
+ // --pro explícito.
634
+ if (args.pro === true) {
635
+ if (access)
636
+ return "pro";
637
+ if (args.yes || !process.stdin.isTTY) {
638
+ throw new Error("você pediu --pro, mas a sua conta não tem acesso à fazer.ai agents Pro (requer membership da comunidade do Lucas Moreira, lucasmoreira.ai). Rode sem --pro para a edição Free.");
639
+ }
640
+ process.stderr.write(`\n${ui_1.sym.warn} A sua conta não tem acesso à fazer.ai agents Pro (requer membership da comunidade do Lucas Moreira).\n`);
641
+ if (!(await (0, ui_select_1.confirm)("Seguir com a edição Free?", true))) {
642
+ throw new Error("onboarding cancelado. Torne-se membro em lucasmoreira.ai e rode de novo com --pro.");
643
+ }
644
+ return "free";
645
+ }
646
+ // --free explícito, ou não-interativo (-y/sem TTY): Free direto, sem upsell.
647
+ if (args.pro === false || args.yes || !process.stdin.isTTY)
648
+ return "free";
649
+ // Sem flag, interativo: pergunta sempre (upsell).
650
+ process.stderr.write(`\n${(0, ui_1.detail)("Edição da fazer.ai agents: Free (1 tenant, imagem pública) ou Pro (multi-tenant, imagem privada; requer membership da comunidade do Lucas Moreira).")}\n`);
651
+ if (!(await (0, ui_select_1.confirm)("Instalar a edição Pro da fazer.ai agents?", false))) {
652
+ return "free";
653
+ }
654
+ if (access)
655
+ return "pro";
656
+ // Quer Pro, mas sem acesso: instrui a comunidade e segue Free.
657
+ process.stderr.write(`\n${(0, ui_1.headline)("fazer.ai agents Pro pela comunidade")}\n`);
658
+ process.stderr.write(`${(0, ui_1.detail)("A edição Pro requer membership da comunidade do Lucas Moreira (lucasmoreira.ai). Abrindo o site (ou acesse o link manualmente). Sigo com a Free por ora; rode de novo com --pro depois de virar membro.")}\n`);
659
+ (0, oauth_1.openBrowser)("https://lucasmoreira.ai");
660
+ return "free";
661
+ }
662
+ // Agente automatizado ausente: roda o instalador oficial e re-detecta. Não pergunta
663
+ // "posso instalar?" (o usuário já escolheu este agente); só anuncia o que vai rodar.
664
+ // Sem installCommand, ou ainda fora do PATH → erro claro. Sem TTY ainda exige --yes:
665
+ // baixar+executar um script remoto sem interação numa automação seria surpreendente.
666
+ async function ensureInstalled(adapter, detection, assumeYes) {
667
+ if (detection.installed)
668
+ return detection;
669
+ const installCmd = (0, agents_1.installCommandFor)(adapter.installCommand);
670
+ if (!installCmd) {
671
+ throw new Error(`${adapter.displayName} não está instalado e não há instalador conhecido para esta plataforma. Instale-o e rode de novo.`);
672
+ }
673
+ if (!assumeYes && !process.stdin.isTTY) {
674
+ throw new Error(`${adapter.displayName} não está instalado. Use um terminal interativo, passe --yes, ou instale você: ${installCmd}.`);
675
+ }
676
+ process.stderr.write(`\n${ui_1.sym.info} Instalando o ${adapter.displayName} (não está instalado):\n`);
677
+ process.stderr.write(`${(0, ui_1.detail)(installCmd)}\n`);
678
+ await (0, exec_1.runShell)(installCmd);
679
+ // Windows: o instalador grava o binário no User PATH persistente, mas esta
680
+ // sessão herdou o PATH antigo. Relê o PATH do registro pra detectar e usar o
681
+ // binário recém-instalado sem precisar reabrir o terminal.
682
+ await (0, exec_1.refreshWindowsPath)();
683
+ const recheck = await adapter.detect();
684
+ if (!recheck.installed) {
685
+ throw new Error(`instalei o ${adapter.displayName}, mas ele não aparece no PATH desta sessão. Adicione a pasta do binário ao seu PATH de usuário e rode o comando de novo.`);
686
+ }
687
+ process.stderr.write(`${ui_1.sym.ok} ${adapter.displayName} instalado${recheck.version ? ` (${recheck.version})` : ""}.\n`);
688
+ return recheck;
689
+ }
690
+ // Garante (best-effort) que o agente está logado. Claude detecta via auth status
691
+ // e abre o login se faltar; Codex/Hermes não têm status confiável, então o fluxo
692
+ // OFERECE o login. Sem TTY, só avisa (o agente pede login no uso).
693
+ async function ensureLoggedIn(adapter) {
694
+ if (!adapter.login)
695
+ return;
696
+ const configured = adapter.configured ? await adapter.configured() : undefined;
697
+ if (configured === true)
698
+ return;
699
+ if (!process.stdin.isTTY) {
700
+ process.stderr.write(`${ui_1.sym.warn} sem terminal interativo para o login do ${adapter.displayName}; ele vai pedir no uso.\n`);
701
+ return;
702
+ }
703
+ if (configured === false) {
704
+ process.stderr.write(`\n${ui_1.sym.info} ${adapter.displayName} sem login. Abrindo o login (conclua e volte)...\n`);
705
+ await adapter.login();
706
+ const recheck = adapter.configured ? await adapter.configured() : undefined;
707
+ if (recheck === false) {
708
+ process.stderr.write(`${ui_1.sym.warn} ${adapter.displayName} segue sem login; ele vai pedir no uso.\n`);
709
+ }
710
+ }
711
+ else {
712
+ // configured indeterminado (Codex/Hermes): oferece o login.
713
+ const yes = await (0, ui_select_1.confirm)(`Não consegui verificar o login do ${adapter.displayName}. Fazer login agora?`);
714
+ if (yes)
715
+ await adapter.login();
716
+ }
717
+ }
718
+ // NOTE: Só executa quando rodado como binário (não no import dos testes, que
719
+ // usam parseArgs/renderPlan diretamente).
720
+ if (require.main === module) {
721
+ main().catch((error) => {
722
+ const message = error instanceof Error ? error.message : String(error);
723
+ process.stderr.write(`${ui_1.sym.err} ${message}\n`);
724
+ process.exit(1);
725
+ });
726
+ }