@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/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
|
+
}
|