@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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/agents/claude.js +152 -0
- package/dist/agents/codex.js +155 -0
- package/dist/agents/detect.js +15 -0
- package/dist/agents/handoff.js +22 -0
- package/dist/agents/hermes-skills.js +177 -0
- package/dist/agents/hermes.js +474 -0
- package/dist/agents/index.js +57 -0
- package/dist/agents/manual.js +22 -0
- package/dist/agents/other.js +39 -0
- package/dist/agents/shell.js +15 -0
- package/dist/agents/types.js +2 -0
- package/dist/config.js +48 -0
- package/dist/exec.js +279 -0
- package/dist/hostinger.js +75 -0
- package/dist/hub-command.js +144 -0
- package/dist/index.js +726 -0
- package/dist/licenses.js +93 -0
- package/dist/mcp.js +100 -0
- package/dist/oauth.js +578 -0
- package/dist/onboarding-marker.js +48 -0
- package/dist/preferences.js +40 -0
- package/dist/skills/agents-dev/SKILL.md +37 -0
- package/dist/skills/agents-dev/gotchas.md +6 -0
- package/dist/skills/agents-dev/guardrails.md +6 -0
- package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
- package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
- package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
- package/dist/skills/agents-dev/references/03-implement.md +9 -0
- package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
- package/dist/skills/agents-onboarding/SKILL.md +80 -0
- package/dist/skills/agents-onboarding/gotchas.md +157 -0
- package/dist/skills/agents-onboarding/guardrails.md +65 -0
- package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
- package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
- package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
- package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
- package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
- package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
- package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
- package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
- package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
- package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
- package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
- package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
- package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
- package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
- package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
- package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
- package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
- package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
- package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
- package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
- package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
- package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
- package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
- package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
- package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
- package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
- package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
- package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
- package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
- package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
- package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
- package/dist/skills/agents-operation/SKILL.md +42 -0
- package/dist/skills/agents-operation/gotchas.md +61 -0
- package/dist/skills/agents-operation/guardrails.md +26 -0
- package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
- package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
- package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
- package/dist/skills/agents-operation/references/03-adjust.md +36 -0
- package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
- package/dist/ui-select.js +279 -0
- package/dist/ui.js +167 -0
- 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
|
+
}
|