@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/exec.js ADDED
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.capture = capture;
7
+ exports.run = run;
8
+ exports.runWithInput = runWithInput;
9
+ exports.windowsPowershell = windowsPowershell;
10
+ exports.shellInvocation = shellInvocation;
11
+ exports.runShell = runShell;
12
+ exports.refreshWindowsPath = refreshWindowsPath;
13
+ exports.withDirOnPath = withDirOnPath;
14
+ exports.ensureUserLocalBinOnPath = ensureUserLocalBinOnPath;
15
+ exports.persistUserLocalBinToWindowsPath = persistUserLocalBinToWindowsPath;
16
+ exports.healPath = healPath;
17
+ exports.runInteractive = runInteractive;
18
+ exports.ptyArgv = ptyArgv;
19
+ exports.handoffInvocation = handoffInvocation;
20
+ exports.runHandoff = runHandoff;
21
+ // cross-spawn (não node:child_process.spawn) resolve os shims do Windows: no
22
+ // Windows os CLIs instalados via npm viram `.cmd`/`.ps1` (ex.: `npm.ps1`,
23
+ // `claude.cmd`), e o spawn nativo sem shell falha com ENOENT (e o Node 20.12+
24
+ // bloqueia `.cmd`/`.bat` sem shell por causa do CVE-2024-27980). cross-spawn faz
25
+ // o lookup PATH×PATHEXT e o quoting de `cmd /c` mantendo o argv literal (sem
26
+ // `shell: true`, que forçaria shell-quoting manual de tokens/prompts).
27
+ const node_fs_1 = require("node:fs");
28
+ const node_os_1 = require("node:os");
29
+ const node_path_1 = require("node:path");
30
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
31
+ const shell_1 = require("./agents/shell");
32
+ // Roda um comando e captura stdout (sem herdar o terminal). Não lança: devolve o
33
+ // código de saída para o chamador decidir. ENOENT/timeout → código != 0.
34
+ function capture(command, args, opts) {
35
+ return new Promise((resolve) => {
36
+ const child = (0, cross_spawn_1.default)(command, args, { stdio: ["ignore", "pipe", "pipe"] });
37
+ let stdout = "";
38
+ let stderr = "";
39
+ let settled = false;
40
+ const finish = (code) => {
41
+ if (settled)
42
+ return;
43
+ settled = true;
44
+ clearTimeout(timer);
45
+ resolve({ code, stdout, stderr });
46
+ };
47
+ const timer = setTimeout(() => {
48
+ child.kill();
49
+ finish(1);
50
+ }, opts?.timeout ?? 5000);
51
+ child.stdout?.on("data", (d) => {
52
+ stdout += d.toString();
53
+ });
54
+ child.stderr?.on("data", (d) => {
55
+ stderr += d.toString();
56
+ });
57
+ // Erro de spawn (binário ausente, ENOENT): não lança, devolve código != 0.
58
+ child.on("error", () => finish(1));
59
+ child.on("close", (code) => finish(typeof code === "number" ? code : 1));
60
+ });
61
+ }
62
+ // Roda um comando herdando stdout/stderr (sem shell: argv passa literal, sem
63
+ // reparse). Rejeita em código de saída != 0. Com `quiet`, captura a saída em vez
64
+ // de imprimir (silencioso no sucesso) e só a anexa ao erro se falhar.
65
+ function run(command, args, opts) {
66
+ return new Promise((resolve, reject) => {
67
+ const std = opts?.quiet ? "pipe" : "inherit";
68
+ const child = (0, cross_spawn_1.default)(command, args, {
69
+ stdio: ["ignore", std, std],
70
+ env: opts?.env ?? process.env,
71
+ });
72
+ let buf = "";
73
+ if (opts?.quiet) {
74
+ child.stdout?.on("data", (d) => {
75
+ buf += d.toString();
76
+ });
77
+ child.stderr?.on("data", (d) => {
78
+ buf += d.toString();
79
+ });
80
+ }
81
+ child.on("error", reject);
82
+ child.on("close", (code) => {
83
+ if (code === 0)
84
+ resolve();
85
+ else {
86
+ const detail = buf.trim() ? `\n${buf.trim().slice(-1500)}` : "";
87
+ reject(new Error(`\`${command} ${args.join(" ")}\` saiu com código ${code ?? "?"}${detail}`));
88
+ }
89
+ });
90
+ });
91
+ }
92
+ // Roda um comando escrevendo `input` no stdin (e fechando-o); stdout/stderr
93
+ // herdados. Pra auto-responder prompts não-evitáveis (ex.: o "Enable all tools?"
94
+ // do `hermes mcp add`, que sem resposta vira "Cancelled"). Rejeita em código != 0.
95
+ function runWithInput(command, args, input, opts) {
96
+ return new Promise((resolve, reject) => {
97
+ const std = opts?.quiet ? "pipe" : "inherit";
98
+ const child = (0, cross_spawn_1.default)(command, args, {
99
+ stdio: ["pipe", std, std],
100
+ env: opts?.env ?? process.env,
101
+ });
102
+ let buf = "";
103
+ if (opts?.quiet) {
104
+ child.stdout?.on("data", (d) => {
105
+ buf += d.toString();
106
+ });
107
+ child.stderr?.on("data", (d) => {
108
+ buf += d.toString();
109
+ });
110
+ }
111
+ child.on("error", reject);
112
+ child.on("close", (code) => {
113
+ if (code === 0)
114
+ resolve();
115
+ else {
116
+ const detail = buf.trim() ? `\n${buf.trim().slice(-1500)}` : "";
117
+ reject(new Error(`\`${command} ${args.join(" ")}\` saiu com código ${code ?? "?"}${detail}`));
118
+ }
119
+ });
120
+ child.stdin?.write(input);
121
+ child.stdin?.end();
122
+ });
123
+ }
124
+ // Windows: powershell.exe nem sempre está no PATH da sessão que roda o CLI (cmd/Git Bash com PATH
125
+ // enxuto, Server Core, etc.), daí o "spawn powershell ENOENT". Resolve o caminho ABSOLUTO do Windows
126
+ // PowerShell 5.1 (sempre em %SystemRoot%\System32\WindowsPowerShell\v1.0); cai no nome puro se o arquivo
127
+ // não existir (ambiente sem PS 5.1). `root`/`exists` injetáveis pra testar. Pura.
128
+ function windowsPowershell(root = process.env.SystemRoot || process.env.windir || "C:\\Windows", exists = node_fs_1.existsSync) {
129
+ const abs = `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
130
+ return exists(abs) ? abs : "powershell";
131
+ }
132
+ // Invocação de shell por plataforma para um instalador em pipe (`curl … | bash`,
133
+ // `irm … | iex`). No Windows os instaladores oficiais dos agentes são PowerShell,
134
+ // então rodamos via `powershell -Command` (o `cmd.exe`, que o `shell: true`
135
+ // usaria, não entende `irm`/`iex`). Em POSIX, `sh -c`. Função pura (testável).
136
+ function shellInvocation(command, platform = process.platform) {
137
+ if (platform === "win32") {
138
+ return {
139
+ cmd: windowsPowershell(),
140
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command],
141
+ };
142
+ }
143
+ return { cmd: "sh", args: ["-c", command] };
144
+ }
145
+ // Roda um instalador em pipe no shell certo da plataforma (ver shellInvocation),
146
+ // herdando stdout/stderr. Rejeita em código de saída != 0.
147
+ function runShell(command) {
148
+ return new Promise((resolve, reject) => {
149
+ const { cmd, args } = shellInvocation(command);
150
+ const child = (0, cross_spawn_1.default)(cmd, args, {
151
+ stdio: ["ignore", "inherit", "inherit"],
152
+ });
153
+ child.on("error", reject);
154
+ child.on("close", (code) => {
155
+ if (code === 0)
156
+ resolve();
157
+ else
158
+ reject(new Error(`o instalador saiu com código ${code ?? "?"}`));
159
+ });
160
+ });
161
+ }
162
+ // Windows: relê o PATH persistente (Machine + User, do registro) e o mescla no
163
+ // process.env.PATH desta sessão. Um instalador (ex.: o `.ps1` de um agente) grava
164
+ // o diretório do binário no User PATH, mas o processo atual herdou o PATH antigo;
165
+ // sem isto, o binário recém-instalado só apareceria num terminal NOVO. No-op fora
166
+ // do Windows; best-effort (falha não interrompe o fluxo).
167
+ async function refreshWindowsPath() {
168
+ if (process.platform !== "win32")
169
+ return;
170
+ const { code, stdout } = await capture(windowsPowershell(), [
171
+ "-NoProfile",
172
+ "-Command",
173
+ "[Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')",
174
+ ]);
175
+ if (code !== 0)
176
+ return;
177
+ const fresh = stdout.trim();
178
+ // Prepend o PATH do registro (que já tem o novo diretório) preservando o atual.
179
+ if (fresh)
180
+ process.env.PATH = `${fresh};${process.env.PATH ?? ""}`;
181
+ }
182
+ // Prepend `dir` em `pathVar` (idempotente). Pura.
183
+ function withDirOnPath(pathVar, dir, sep) {
184
+ if (!dir)
185
+ return pathVar;
186
+ if (pathVar.split(sep).includes(dir))
187
+ return pathVar;
188
+ return pathVar ? `${dir}${sep}${pathVar}` : dir;
189
+ }
190
+ // `~/.local/bin`: onde os instaladores nativos (Claude, Hermes) gravam o binário SEM
191
+ // adicioná-lo ao PATH persistente, então o refreshWindowsPath (que só relê o registro) não o
192
+ // pega. Prepend na sessão pra o detect do startup E o re-detect pós-install acharem o binário
193
+ // sem depender de reabrir o terminal. Idempotente; no-op se já estiver no PATH.
194
+ function ensureUserLocalBinOnPath() {
195
+ const sep = process.platform === "win32" ? ";" : ":";
196
+ process.env.PATH = withDirOnPath(process.env.PATH ?? "", (0, node_path_1.join)((0, node_os_1.homedir)(), ".local", "bin"), sep);
197
+ }
198
+ // Windows: PERSISTE `~/.local/bin` no User PATH do registro (idempotente). O ensureUserLocalBinOnPath
199
+ // acima só conserta o PATH DESTA sessão do CLI; a etapa de OAuth do MCP no onboarding manda o usuário
200
+ // REABRIR o agente num terminal NOVO, que só acha o binário se ~/.local/bin estiver no PATH persistente.
201
+ // Os instaladores nativos gravam lá sem tocar no PATH, então persistimos por eles. No-op fora do Windows;
202
+ // best-effort (capture não lança).
203
+ async function persistUserLocalBinToWindowsPath() {
204
+ if (process.platform !== "win32")
205
+ return;
206
+ const dir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".local", "bin");
207
+ // Lê o User PATH (coage null→''), anexa o dir só se ausente (case-insensitive) e regrava.
208
+ const script = `$d = '${dir}'; ` +
209
+ "$p = [string][Environment]::GetEnvironmentVariable('Path','User'); " +
210
+ "if (-not (($p -split ';') -contains $d)) { " +
211
+ "[Environment]::SetEnvironmentVariable('Path', (($p.TrimEnd(';') + ';' + $d).TrimStart(';')), 'User') }";
212
+ await capture(windowsPowershell(), ["-NoProfile", "-Command", script]);
213
+ }
214
+ // Auto-cura do PATH no startup, pra o CLI achar as ferramentas que spawna (npm/npx ao lado do node;
215
+ // powershell e os agentes no PATH persistente) mesmo numa sessão com PATH mínimo/desatualizado; a raiz
216
+ // dos "spawn npm/powershell ENOENT". (1) Prepend do dir do runtime atual (process.execPath: onde o
217
+ // npm/npx vivem ao lado do node). (2) No Windows, relê o PATH persistente do registro (Machine+User).
218
+ // Best-effort e idempotente.
219
+ async function healPath() {
220
+ const sep = process.platform === "win32" ? ";" : ":";
221
+ process.env.PATH = withDirOnPath(process.env.PATH ?? "", (0, node_path_1.dirname)(process.execPath), sep);
222
+ await refreshWindowsPath();
223
+ }
224
+ // Roda um sub-comando interativo do agente (login/auth/setup) herdando o terminal. Esses rodam DURANTE a
225
+ // fase interativa do CLI (que já tem o TTY) e não são a TUI longa do handoff. O código de saída não é erro
226
+ // do CLI. Para a TUI FINAL do handoff use runHandoff (precisa de pty), não esta.
227
+ function runInteractive(command, args, opts) {
228
+ return new Promise((resolve, reject) => {
229
+ const child = (0, cross_spawn_1.default)(command, args, {
230
+ stdio: "inherit",
231
+ env: opts?.env ?? process.env,
232
+ });
233
+ child.on("error", reject);
234
+ child.on("close", () => resolve());
235
+ });
236
+ }
237
+ // script(1) por plataforma para alocar um pty. BSD/macOS: `script -q /dev/null cmd args…` (argv direto,
238
+ // sem re-quote). util-linux/Linux: `script -q -c "<cmd string>" /dev/null` (o comando é UMA string de
239
+ // shell, daí o shellQuote). Fora de darwin/linux (ex.: Windows) não há script equivalente → undefined, e
240
+ // o handoff cai no caminho manual (imprime o comando) em vez de arriscar um crash. Pura (testável).
241
+ function ptyArgv(command, args, platform = process.platform) {
242
+ if (platform === "darwin") {
243
+ return { cmd: "script", argv: ["-q", "/dev/null", command, ...args] };
244
+ }
245
+ if (platform === "linux") {
246
+ const line = [command, ...args].map(shell_1.shellQuote).join(" ");
247
+ return { cmd: "script", argv: ["-q", "-c", line, "/dev/null"] };
248
+ }
249
+ return undefined;
250
+ }
251
+ // Decide COMO abrir a TUI do handoff por plataforma. Windows: spawn DIRETO (cmd/argv via cross-spawn),
252
+ // porque o filho herda o console real do PowerShell e o crossterm do Codex inicia sem panic (validado num
253
+ // box Windows); não há script(1) e nem é preciso. POSIX: pty via script(1) (ptyArgv), porque o instalador
254
+ // roda via `curl | bash` e o pipe quebra o tty (o crossterm panica "reader source not set"). Devolve
255
+ // undefined só em POSIX sem script equivalente (cai no caminho manual). Pura (testável).
256
+ function handoffInvocation(command, args, platform = process.platform) {
257
+ if (platform === "win32")
258
+ return { cmd: command, argv: args };
259
+ return ptyArgv(command, args, platform);
260
+ }
261
+ // Handoff final: entrega o terminal à TUI do agente; devolve `true` se conseguiu ABRIR. A invocação por
262
+ // plataforma vem de handoffInvocation (Windows: spawn direto, console herdado; POSIX: pty via script(1),
263
+ // que o `curl | bash` exige, sem ele o crossterm do Codex panica "reader source not set"). Sem invocação
264
+ // possível, ou se o spawn falhar ('error'), devolve `false` SEM crashar: o chamador imprime o comando pro
265
+ // usuário abrir à mão. O código de saída do agente não é erro do CLI.
266
+ function runHandoff(command, args, opts) {
267
+ const inv = handoffInvocation(command, args);
268
+ if (!inv)
269
+ return Promise.resolve(false);
270
+ return new Promise((resolve) => {
271
+ const child = (0, cross_spawn_1.default)(inv.cmd, inv.argv, {
272
+ stdio: "inherit",
273
+ env: opts?.env ?? process.env,
274
+ });
275
+ // 'error' = nem iniciou (binário ausente etc.) → false; o chamador imprime o caminho manual.
276
+ child.on("error", () => resolve(false));
277
+ child.on("close", () => resolve(true));
278
+ });
279
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hostingerTokenPath = hostingerTokenPath;
4
+ exports.loadHostingerToken = loadHostingerToken;
5
+ exports.saveHostingerToken = saveHostingerToken;
6
+ exports.classifyTokenStatus = classifyTokenStatus;
7
+ exports.validateHostingerToken = validateHostingerToken;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_https_1 = require("node:https");
10
+ const node_os_1 = require("node:os");
11
+ const node_path_1 = require("node:path");
12
+ // Cache opt-in do token da API Hostinger (~/.fazer-ai/hostinger.json, 0600). É
13
+ // uma chave poderosa (VPS/DNS/domínio), então só é gravada se o usuário aceitar.
14
+ function hostingerTokenPath(home = (0, node_os_1.homedir)()) {
15
+ return (0, node_path_1.join)(home, ".fazer-ai", "hostinger.json");
16
+ }
17
+ function loadHostingerToken(path = hostingerTokenPath()) {
18
+ try {
19
+ const obj = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
20
+ return typeof obj.token === "string" && obj.token ? obj.token : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function saveHostingerToken(token, path = hostingerTokenPath()) {
27
+ try {
28
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
29
+ (0, node_fs_1.writeFileSync)(path, `${JSON.stringify({ token }, null, 2)}\n`, {
30
+ mode: 0o600,
31
+ });
32
+ (0, node_fs_1.chmodSync)(path, 0o600);
33
+ }
34
+ catch {
35
+ // cache é best-effort; falhar aqui não quebra o fluxo.
36
+ }
37
+ }
38
+ // Classifica o status HTTP da validação do token: 2xx = válido; 401/403 =
39
+ // inválido/expirado/sem permissão; o resto (5xx, rede, timeout) = "error"
40
+ // (indeterminado, o chamador NÃO deve rejeitar o token por isto). Pura (testável).
41
+ function classifyTokenStatus(code) {
42
+ if (code >= 200 && code < 300)
43
+ return true;
44
+ if (code === 401 || code === 403)
45
+ return false;
46
+ return "error";
47
+ }
48
+ // Valida o token contra a API Hostinger via um GET autenticado leve
49
+ // (`/api/vps/v1/virtual-machines`): 401 confirma token inválido/expirado. true =
50
+ // válido, false = inválido, "error" = não deu pra validar (rede/timeout/5xx),
51
+ // caso em que o chamador aceita o token (best-effort), pra não travar por um
52
+ // problema transitório.
53
+ function validateHostingerToken(token, timeoutMs = 10000) {
54
+ return new Promise((resolve) => {
55
+ const req = (0, node_https_1.request)({
56
+ method: "GET",
57
+ hostname: "developers.hostinger.com",
58
+ path: "/api/vps/v1/virtual-machines",
59
+ headers: {
60
+ authorization: `Bearer ${token}`,
61
+ accept: "application/json",
62
+ "user-agent": "@fazer-ai/agents",
63
+ },
64
+ }, (res) => {
65
+ res.resume(); // descarta o corpo
66
+ resolve(classifyTokenStatus(res.statusCode ?? 0));
67
+ });
68
+ req.setTimeout(timeoutMs, () => {
69
+ req.destroy();
70
+ resolve("error");
71
+ });
72
+ req.on("error", () => resolve("error"));
73
+ req.end();
74
+ });
75
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseFlags = parseFlags;
4
+ exports.resolveHubOp = resolveHubOp;
5
+ exports.harborSecretPath = harborSecretPath;
6
+ exports.runHubCommand = runHubCommand;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const oauth_1 = require("./oauth");
11
+ // "--key value" / "--flag" → mapa. Pura.
12
+ function parseFlags(argv) {
13
+ const out = {};
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i];
16
+ if (a === undefined || !a.startsWith("--"))
17
+ continue;
18
+ const key = a.slice(2);
19
+ const next = argv[i + 1];
20
+ if (next === undefined || next.startsWith("--")) {
21
+ out[key] = true;
22
+ }
23
+ else {
24
+ out[key] = next;
25
+ i++;
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+ // Mapeia o subcomando + flags na tool do hub + argumentos. Pura + testável; lança com mensagem clara
31
+ // quando falta uma flag obrigatória ou o op é desconhecido.
32
+ function resolveHubOp(op, flags) {
33
+ const apply = flags.apply === true;
34
+ const need = (k) => {
35
+ const v = flags[k];
36
+ if (typeof v !== "string" || !v) {
37
+ throw new Error(`--${k} é obrigatório para 'hub ${op}'`);
38
+ }
39
+ return v;
40
+ };
41
+ switch (op) {
42
+ case "whoami":
43
+ return { tool: "whoami", args: {}, write: false, secret: false };
44
+ case "licenses":
45
+ return { tool: "list_licenses", args: {}, write: false, secret: false };
46
+ case "instances":
47
+ return { tool: "list_instances", args: {}, write: false, secret: false };
48
+ case "get-license":
49
+ return {
50
+ tool: "get_license",
51
+ args: { license_id: need("license") },
52
+ write: false,
53
+ secret: false,
54
+ };
55
+ case "get-instance":
56
+ return {
57
+ tool: "get_instance",
58
+ args: { instance_id: need("instance") },
59
+ write: false,
60
+ secret: false,
61
+ };
62
+ case "registry-credential": {
63
+ // Per-user por padrão (o hub aceita sem license_id e devolve o robot per-user); --license restringe.
64
+ const args = { dry_run: !apply };
65
+ if (typeof flags.license === "string" && flags.license) {
66
+ args.license_id = flags.license;
67
+ }
68
+ return { tool: "create_registry_credential", args, write: true, secret: true };
69
+ }
70
+ case "create-instance": {
71
+ const args = {
72
+ identifier: need("identifier"),
73
+ dry_run: !apply,
74
+ };
75
+ if (typeof flags.name === "string" && flags.name)
76
+ args.name = flags.name;
77
+ return { tool: "create_instance", args, write: true, secret: false };
78
+ }
79
+ case "attach-license":
80
+ return {
81
+ tool: "attach_license",
82
+ args: {
83
+ license_id: need("license"),
84
+ instance_id: need("instance"),
85
+ dry_run: !apply,
86
+ },
87
+ write: true,
88
+ secret: false,
89
+ };
90
+ default:
91
+ throw new Error(`subcomando 'hub ${op}' desconhecido (use: whoami | licenses | instances | get-license | get-instance | registry-credential | create-instance | attach-license)`);
92
+ }
93
+ }
94
+ function harborSecretPath(home = (0, node_os_1.homedir)()) {
95
+ return (0, node_path_1.join)(home, ".fazer-ai", "harbor.secret");
96
+ }
97
+ const USAGE = `Uso: agents hub <op> [flags]
98
+
99
+ Proxy do hub fazer.ai para o onboarding (usa a sessão OAuth do ~/.fazer-ai/oauth.json).
100
+ Writes são DRY-RUN por padrão; aplique com --apply.
101
+
102
+ whoami identidade + escopos do usuário (read)
103
+ licenses lista as licenças (read)
104
+ instances lista as instâncias (read)
105
+ get-license --license <id> detalha uma licença (read)
106
+ get-instance --instance <id> detalha uma instância (read)
107
+ registry-credential [--license <id>] [--apply] credencial do Harbor; --apply grava o secret num
108
+ arquivo 0600 (--out) e NÃO o imprime
109
+ create-instance --identifier <host> [--name <n>] [--apply]
110
+ attach-license --license <id> --instance <id> [--apply]
111
+ `;
112
+ // Runner (IO). Chamado pelo index.ts quando argv[0] === "hub". Imprime JSON no stdout (o agente lê).
113
+ async function runHubCommand(config, argv, log) {
114
+ const op = argv[0];
115
+ if (!op || op === "--help" || op === "-h" || op === "help") {
116
+ process.stdout.write(USAGE);
117
+ return;
118
+ }
119
+ const flags = parseFlags(argv.slice(1));
120
+ const spec = resolveHubOp(op, flags);
121
+ // refreshOnly: o agente roda headless; se a sessão do hub expirou, falha rápido (o operador re-roda o CLI).
122
+ const result = await (0, oauth_1.hubCall)(config, { log, refreshOnly: true }, spec.tool, spec.args);
123
+ if (spec.secret && spec.args.dry_run === false) {
124
+ const secretVal = typeof result.secret === "string" ? result.secret : undefined;
125
+ const username = typeof result.username === "string" ? result.username : undefined;
126
+ if (secretVal) {
127
+ const out = typeof flags.out === "string" && flags.out
128
+ ? flags.out
129
+ : harborSecretPath();
130
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(out), { recursive: true, mode: 0o700 });
131
+ (0, node_fs_1.writeFileSync)(out, secretVal, { mode: 0o600 });
132
+ (0, node_fs_1.chmodSync)(out, 0o600);
133
+ process.stdout.write(`${JSON.stringify({
134
+ ok: true,
135
+ registry: "harbor.fazer.ai",
136
+ username,
137
+ secret_file: out,
138
+ note: "secret gravado em arquivo (0600), não impresso",
139
+ })}\n`);
140
+ return;
141
+ }
142
+ }
143
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
144
+ }