@hacloud/cli 0.1.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/README.md +44 -0
- package/dist/hacli.mjs +706 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# hacli — CLI da Hospede Aqui
|
|
2
|
+
|
|
3
|
+
Deploy, logs em tempo real, variáveis, rollback e previews dos seus apps na [Hospede Aqui](https://hospedeaqui.cloud) pelo terminal ou no CI. Disponível em todos os planos.
|
|
4
|
+
|
|
5
|
+
Requer Node.js 20 ou mais novo.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @hacloud/cli
|
|
9
|
+
hacli login # autoriza no navegador (OAuth)
|
|
10
|
+
hacli link minha-api # vincula a pasta a um app
|
|
11
|
+
hacli deploy --wait # faz o deploy e acompanha o build
|
|
12
|
+
hacli logs -f # logs em tempo real, com segredos redigidos
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Sem instalar: `npx @hacloud/cli login`.
|
|
16
|
+
|
|
17
|
+
## Comandos
|
|
18
|
+
|
|
19
|
+
| Comando | O que faz |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `hacli login` / `logout` / `whoami` | Conta e sessão |
|
|
22
|
+
| `hacli apps` | Lista os apps da organização |
|
|
23
|
+
| `hacli link <app>` | Vincula a pasta atual a um app (`.hospedeaqui.json`) |
|
|
24
|
+
| `hacli status` | Status do app e do último deploy |
|
|
25
|
+
| `hacli deploy [--wait] [--no-cache]` | Agenda um deploy; `--wait` acompanha e falha se o deploy falhar |
|
|
26
|
+
| `hacli logs [-f] [-n 200] [--deployment <id>]` | Logs de runtime ou de um deploy |
|
|
27
|
+
| `hacli env ls` / `set K=v` / `unset K` | Variáveis do app (valores sempre mascarados) |
|
|
28
|
+
| `hacli rollback [deploymentId]` | Volta para o deploy anterior que funcionou |
|
|
29
|
+
| `hacli restart` · `hacli open` · `hacli previews` | Reinicia, abre no navegador, lista previews de PR |
|
|
30
|
+
|
|
31
|
+
Use `--app <nome>` para escolher o app sem vincular a pasta.
|
|
32
|
+
|
|
33
|
+
## CI
|
|
34
|
+
|
|
35
|
+
Crie um token de acesso em **Integrações › MCP** (escopos `platform:read`, `apps:read`, `apps:write`, `deployments:write`) e salve como segredo `HOSPEDEAQUI_TOKEN`:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
- name: Deploy na Hospede Aqui
|
|
39
|
+
run: npx -y @hacloud/cli deploy --app minha-api --wait
|
|
40
|
+
env:
|
|
41
|
+
HOSPEDEAQUI_TOKEN: ${{ secrets.HOSPEDEAQUI_TOKEN }}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Documentação: https://hospedeaqui.cloud/cli
|
package/dist/hacli.mjs
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
// src/api.ts
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
// src/auth.ts
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
12
|
+
import { join as join2 } from "node:path";
|
|
13
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
14
|
+
import { createServer } from "node:http";
|
|
15
|
+
|
|
16
|
+
// src/config.ts
|
|
17
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
var DEFAULT_API_URL = "https://api.hospedeaqui.cloud";
|
|
21
|
+
var LINK_FILE = ".hospedeaqui.json";
|
|
22
|
+
function apiUrl(env = process.env) {
|
|
23
|
+
return (env.HOSPEDEAQUI_API_URL?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
function configDir(env = process.env, platform = process.platform) {
|
|
26
|
+
if (platform === "win32") return join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "hospedeaqui");
|
|
27
|
+
return join(env.XDG_CONFIG_HOME || join(homedir(), ".config"), "hospedeaqui");
|
|
28
|
+
}
|
|
29
|
+
var credentialsPath = () => join(configDir(), "credentials.json");
|
|
30
|
+
function readCredentials() {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(credentialsPath(), "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function writeCredentials(credentials) {
|
|
38
|
+
const path = credentialsPath();
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
40
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
41
|
+
writeFileSync(tmp, JSON.stringify(credentials, null, 2), { mode: 384 });
|
|
42
|
+
try {
|
|
43
|
+
chmodSync(tmp, 384);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
renameSync(tmp, path);
|
|
47
|
+
}
|
|
48
|
+
function clearCredentials() {
|
|
49
|
+
rmSync(credentialsPath(), { force: true });
|
|
50
|
+
}
|
|
51
|
+
function findLink(start = process.cwd()) {
|
|
52
|
+
let dir = resolve(start);
|
|
53
|
+
for (; ; ) {
|
|
54
|
+
const path = join(dir, LINK_FILE);
|
|
55
|
+
if (existsSync(path)) {
|
|
56
|
+
try {
|
|
57
|
+
return { path, link: JSON.parse(readFileSync(path, "utf8")) };
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(dir);
|
|
63
|
+
if (parent === dir) return null;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeLink(link, dir = process.cwd()) {
|
|
68
|
+
const path = join(dir, LINK_FILE);
|
|
69
|
+
writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
|
|
70
|
+
return path;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/output.ts
|
|
74
|
+
var CliError = class extends Error {
|
|
75
|
+
constructor(message, exitCode = 1) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.exitCode = exitCode;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
81
|
+
var paint = (code) => (text) => useColor ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
82
|
+
var color = { dim: paint(2), bold: paint(1), red: paint(31), green: paint(32), yellow: paint(33), cyan: paint(36) };
|
|
83
|
+
function statusColor(status) {
|
|
84
|
+
if (["success", "running", "active", "succeeded", "ready"].includes(status)) return color.green(status);
|
|
85
|
+
if (["failed", "error", "cancelled", "rejected", "crashed"].includes(status)) return color.red(status);
|
|
86
|
+
return color.yellow(status);
|
|
87
|
+
}
|
|
88
|
+
function table(headers, rows) {
|
|
89
|
+
const visible = (s) => s.replace(/\u001b\[\d+m/g, "").length;
|
|
90
|
+
const widths = headers.map((h, i) => Math.max(visible(h), ...rows.map((r) => visible(r[i] ?? ""))));
|
|
91
|
+
const line = (cells) => cells.map((c, i) => c + " ".repeat(widths[i] - visible(c))).join(" ").trimEnd();
|
|
92
|
+
return [color.bold(line(headers)), ...rows.map(line)].join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/auth.ts
|
|
96
|
+
var CLI_SCOPES = "platform:read apps:read apps:write deployments:write";
|
|
97
|
+
var LOGIN_TIMEOUT_MS = 5 * 6e4;
|
|
98
|
+
function pkcePair() {
|
|
99
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
100
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
101
|
+
return { verifier, challenge };
|
|
102
|
+
}
|
|
103
|
+
function authorizeUrl(base, input) {
|
|
104
|
+
const url = new URL(`${base}/oauth/authorize`);
|
|
105
|
+
url.search = new URLSearchParams({
|
|
106
|
+
response_type: "code",
|
|
107
|
+
client_id: input.clientId,
|
|
108
|
+
redirect_uri: input.redirectUri,
|
|
109
|
+
code_challenge: input.challenge,
|
|
110
|
+
code_challenge_method: "S256",
|
|
111
|
+
state: input.state,
|
|
112
|
+
resource: `${base}/mcp`,
|
|
113
|
+
scope: CLI_SCOPES
|
|
114
|
+
}).toString();
|
|
115
|
+
return url.toString();
|
|
116
|
+
}
|
|
117
|
+
var AUTH_TIMEOUT_MS = 15e3;
|
|
118
|
+
async function postForm(url, body) {
|
|
119
|
+
const response = await fetch(url, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
122
|
+
body: new URLSearchParams(body).toString(),
|
|
123
|
+
signal: AbortSignal.timeout(AUTH_TIMEOUT_MS)
|
|
124
|
+
});
|
|
125
|
+
const data = await response.json().catch(() => ({}));
|
|
126
|
+
if (!response.ok) throw new CliError(`Falha na autentica\xE7\xE3o: ${String(data.error_description ?? data.error ?? response.status)}`);
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
function browserCommand(url, platform = process.platform) {
|
|
130
|
+
if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
|
|
131
|
+
if (platform === "darwin") return ["open", [url]];
|
|
132
|
+
return ["xdg-open", [url]];
|
|
133
|
+
}
|
|
134
|
+
function openBrowser(url) {
|
|
135
|
+
const [command, args] = browserCommand(url);
|
|
136
|
+
try {
|
|
137
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
138
|
+
child.on("error", () => {
|
|
139
|
+
});
|
|
140
|
+
child.unref();
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function login(log) {
|
|
145
|
+
const base = apiUrl();
|
|
146
|
+
const state = randomBytes(16).toString("base64url");
|
|
147
|
+
const { verifier, challenge } = pkcePair();
|
|
148
|
+
let resolveCode, rejectCode;
|
|
149
|
+
const codePromise = new Promise((ok, fail) => {
|
|
150
|
+
resolveCode = ok;
|
|
151
|
+
rejectCode = fail;
|
|
152
|
+
});
|
|
153
|
+
const server = createServer((req, res) => {
|
|
154
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
155
|
+
if (url.pathname !== "/callback") {
|
|
156
|
+
res.writeHead(404).end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const error = url.searchParams.get("error");
|
|
160
|
+
const ok = !error && url.searchParams.get("state") === state && url.searchParams.get("code");
|
|
161
|
+
res.writeHead(ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
|
|
162
|
+
res.end(`<!doctype html><meta charset="utf-8"><title>Hospede Aqui CLI</title><body style="font-family:system-ui;padding:40px">
|
|
163
|
+
<h2>${ok ? "Pronto! Pode fechar esta aba e voltar ao terminal." : "N\xE3o foi poss\xEDvel autorizar a CLI."}</h2></body>`);
|
|
164
|
+
if (ok) resolveCode(url.searchParams.get("code"));
|
|
165
|
+
else rejectCode(new CliError(error === "access_denied" ? "Autoriza\xE7\xE3o recusada no navegador." : "Resposta inv\xE1lida do login (state n\xE3o confere)."));
|
|
166
|
+
});
|
|
167
|
+
await new Promise((ok) => server.listen(0, "127.0.0.1", ok));
|
|
168
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
169
|
+
const timer = setTimeout(() => rejectCode(new CliError("Tempo de login esgotado (5 min).")), LOGIN_TIMEOUT_MS);
|
|
170
|
+
try {
|
|
171
|
+
const client = await postJson(`${base}/oauth/register`, {
|
|
172
|
+
client_name: "Hospede Aqui CLI",
|
|
173
|
+
redirect_uris: [redirectUri],
|
|
174
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
175
|
+
response_types: ["code"],
|
|
176
|
+
token_endpoint_auth_method: "none"
|
|
177
|
+
});
|
|
178
|
+
const url = authorizeUrl(base, { clientId: client.client_id, redirectUri, challenge, state });
|
|
179
|
+
log("Abrindo o navegador para autorizar a CLI. Se n\xE3o abrir, acesse:");
|
|
180
|
+
log(url);
|
|
181
|
+
openBrowser(url);
|
|
182
|
+
const code = await codePromise;
|
|
183
|
+
const token = await postForm(`${base}/oauth/token`, {
|
|
184
|
+
grant_type: "authorization_code",
|
|
185
|
+
code,
|
|
186
|
+
redirect_uri: redirectUri,
|
|
187
|
+
code_verifier: verifier,
|
|
188
|
+
client_id: client.client_id,
|
|
189
|
+
resource: `${base}/mcp`
|
|
190
|
+
});
|
|
191
|
+
writeCredentials({
|
|
192
|
+
apiUrl: base,
|
|
193
|
+
clientId: client.client_id,
|
|
194
|
+
accessToken: token.access_token,
|
|
195
|
+
refreshToken: token.refresh_token,
|
|
196
|
+
expiresAt: Date.now() + token.expires_in * 1e3,
|
|
197
|
+
scope: token.scope
|
|
198
|
+
});
|
|
199
|
+
} finally {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
server.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function postJson(url, body) {
|
|
205
|
+
const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(body) });
|
|
206
|
+
const data = await response.json().catch(() => ({}));
|
|
207
|
+
if (!response.ok) throw new CliError(`Falha ao registrar a CLI: ${String(data.error_description ?? data.error ?? response.status)}`);
|
|
208
|
+
return data;
|
|
209
|
+
}
|
|
210
|
+
async function accessToken(options = {}) {
|
|
211
|
+
const pat = process.env.HOSPEDEAQUI_TOKEN?.trim();
|
|
212
|
+
if (pat) return pat;
|
|
213
|
+
const stored = readCredentials();
|
|
214
|
+
if (!stored) throw new CliError("Voc\xEA n\xE3o est\xE1 logado. Rode `hacli login` (ou defina HOSPEDEAQUI_TOKEN com um token de acesso).");
|
|
215
|
+
if (stored.apiUrl !== apiUrl()) throw new CliError(`O login salvo \xE9 de ${stored.apiUrl}. Rode \`hacli login\` de novo.`);
|
|
216
|
+
if (!options.forceRefresh && stored.expiresAt - 6e4 > Date.now()) return stored.accessToken;
|
|
217
|
+
refreshing ??= refreshOnce(stored.accessToken).finally(() => {
|
|
218
|
+
refreshing = null;
|
|
219
|
+
});
|
|
220
|
+
return refreshing;
|
|
221
|
+
}
|
|
222
|
+
var refreshing = null;
|
|
223
|
+
var LOCK_STALE_MS = 6e4;
|
|
224
|
+
async function withRefreshLock(fn) {
|
|
225
|
+
const lock = join2(configDir(), "refresh.lock");
|
|
226
|
+
const ownerFile = join2(lock, "owner");
|
|
227
|
+
const token = `${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
228
|
+
mkdirSync2(configDir(), { recursive: true, mode: 448 });
|
|
229
|
+
const deadline = Date.now() + 3e4;
|
|
230
|
+
for (; ; ) {
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync2(lock);
|
|
233
|
+
writeFileSync2(ownerFile, token);
|
|
234
|
+
break;
|
|
235
|
+
} catch {
|
|
236
|
+
try {
|
|
237
|
+
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) rmSync2(lock, { recursive: true, force: true });
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
if (Date.now() > deadline) throw new CliError("Outra execu\xE7\xE3o da CLI est\xE1 renovando o login. Tente de novo.");
|
|
241
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
return await fn();
|
|
246
|
+
} finally {
|
|
247
|
+
try {
|
|
248
|
+
if (readFileSync2(ownerFile, "utf8") === token) rmSync2(lock, { recursive: true, force: true });
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function refreshOnce(staleAccessToken) {
|
|
254
|
+
return withRefreshLock(async () => {
|
|
255
|
+
const stored = readCredentials();
|
|
256
|
+
if (!stored) throw new CliError("Voc\xEA n\xE3o est\xE1 logado. Rode `hacli login`.");
|
|
257
|
+
if (stored.accessToken !== staleAccessToken && stored.expiresAt - 6e4 > Date.now()) return stored.accessToken;
|
|
258
|
+
let token;
|
|
259
|
+
try {
|
|
260
|
+
token = await postForm(`${stored.apiUrl}/oauth/token`, {
|
|
261
|
+
grant_type: "refresh_token",
|
|
262
|
+
refresh_token: stored.refreshToken,
|
|
263
|
+
client_id: stored.clientId,
|
|
264
|
+
resource: `${stored.apiUrl}/mcp`
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
clearCredentials();
|
|
268
|
+
throw new CliError("Sua sess\xE3o expirou ou foi revogada. Rode `hacli login` de novo.");
|
|
269
|
+
}
|
|
270
|
+
const next = {
|
|
271
|
+
...stored,
|
|
272
|
+
accessToken: token.access_token,
|
|
273
|
+
refreshToken: token.refresh_token,
|
|
274
|
+
expiresAt: Date.now() + token.expires_in * 1e3,
|
|
275
|
+
scope: token.scope
|
|
276
|
+
};
|
|
277
|
+
writeCredentials(next);
|
|
278
|
+
return next.accessToken;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
async function logout() {
|
|
282
|
+
const stored = readCredentials();
|
|
283
|
+
if (!stored) return "none";
|
|
284
|
+
const revoke = (token) => fetch(`${stored.apiUrl}/oauth/revoke`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
287
|
+
body: new URLSearchParams({ token, client_id: stored.clientId }).toString()
|
|
288
|
+
}).then((r) => r.ok).catch(() => false);
|
|
289
|
+
const results = await Promise.all([revoke(stored.refreshToken), revoke(stored.accessToken)]);
|
|
290
|
+
clearCredentials();
|
|
291
|
+
return results.every(Boolean) ? "revoked" : "local_only";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/api.ts
|
|
295
|
+
var nextId = 1;
|
|
296
|
+
function toolPayload(result) {
|
|
297
|
+
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
298
|
+
if (result.isError) throw new CliError(text || "A opera\xE7\xE3o falhou.");
|
|
299
|
+
if (result.structuredContent !== void 0) return result.structuredContent;
|
|
300
|
+
if (!text) return null;
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(text);
|
|
303
|
+
} catch {
|
|
304
|
+
return text;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function callTool(name, args = {}) {
|
|
308
|
+
const token = await accessToken();
|
|
309
|
+
const response = await fetch(`${apiUrl()}/mcp`, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
312
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: nextId++, method: "tools/call", params: { name, arguments: args } })
|
|
313
|
+
});
|
|
314
|
+
if (response.status === 401) throw new CliError("Token inv\xE1lido, expirado ou revogado. Rode `hacli login` (ou confira HOSPEDEAQUI_TOKEN).");
|
|
315
|
+
if (response.status === 429) throw new CliError("Muitas requisi\xE7\xF5es. Aguarde um minuto e tente de novo.");
|
|
316
|
+
const data = await response.json().catch(() => null);
|
|
317
|
+
if (!response.ok || !data) throw new CliError(`A API respondeu ${response.status}.`);
|
|
318
|
+
if (data.error) throw new CliError(data.error.message || "Erro na API.");
|
|
319
|
+
return toolPayload(data.result ?? {});
|
|
320
|
+
}
|
|
321
|
+
async function runMutation(name, args, timeoutMs = 6e4) {
|
|
322
|
+
const queued = await callTool(name, { idempotencyKey: `cli-${randomUUID()}`, ...args });
|
|
323
|
+
if (!queued?.operationId) return queued;
|
|
324
|
+
const deadline = Date.now() + timeoutMs;
|
|
325
|
+
for (; ; ) {
|
|
326
|
+
const operation = await callTool("operation_get", { operationId: queued.operationId });
|
|
327
|
+
if (operation.status === "succeeded") return operation.result;
|
|
328
|
+
if (operation.status === "failed" || operation.status === "cancelled") throw new CliError(operation.error?.message || `Opera\xE7\xE3o ${operation.status}.`);
|
|
329
|
+
if (Date.now() > deadline) throw new CliError(`A opera\xE7\xE3o ainda est\xE1 em andamento (${queued.operationId}).`);
|
|
330
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
var StreamTokenExpired = class extends Error {
|
|
334
|
+
};
|
|
335
|
+
async function streamEvents(path, onEvent, signal, options = {}) {
|
|
336
|
+
const token = await accessToken(options);
|
|
337
|
+
const response = await fetch(`${apiUrl()}${path}`, { headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }, signal });
|
|
338
|
+
if (response.status === 401) throw new CliError("Token inv\xE1lido, expirado ou revogado. Rode `hacli login`.");
|
|
339
|
+
if (!response.ok || !response.body) {
|
|
340
|
+
const body = await response.json().catch(() => ({}));
|
|
341
|
+
throw new CliError(body.message || `A API respondeu ${response.status}.`);
|
|
342
|
+
}
|
|
343
|
+
const decoder = new TextDecoder();
|
|
344
|
+
let buffer = "";
|
|
345
|
+
for await (const chunk of response.body) {
|
|
346
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
347
|
+
const { events, rest } = parseSse(buffer);
|
|
348
|
+
buffer = rest;
|
|
349
|
+
for (const event of events) {
|
|
350
|
+
if (event.type === "error" && event.code === "token_expired") throw new StreamTokenExpired();
|
|
351
|
+
if (onEvent(event) === false) return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function parseSse(buffer) {
|
|
356
|
+
const parts = buffer.replace(/\r\n/g, "\n").split("\n\n");
|
|
357
|
+
const rest = parts.pop() ?? "";
|
|
358
|
+
const events = [];
|
|
359
|
+
for (const part of parts) {
|
|
360
|
+
const data = part.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
361
|
+
if (!data) continue;
|
|
362
|
+
try {
|
|
363
|
+
events.push(JSON.parse(data));
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { events, rest };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/commands/apps.ts
|
|
371
|
+
var appUrl = (app) => app.customDomain || app.subdomain ? `https://${app.customDomain || app.subdomain}` : null;
|
|
372
|
+
async function listApps() {
|
|
373
|
+
const data = await callTool("app_list");
|
|
374
|
+
return (data.apps ?? []).filter((app) => !app.previewOfAppId);
|
|
375
|
+
}
|
|
376
|
+
async function resolveApp(flag) {
|
|
377
|
+
if (flag) {
|
|
378
|
+
const apps = await listApps();
|
|
379
|
+
const found = apps.find((app) => app.id === flag || app.name === flag);
|
|
380
|
+
if (!found) throw new CliError(`App "${flag}" n\xE3o encontrado. Veja a lista com \`hacli apps\`.`);
|
|
381
|
+
return found;
|
|
382
|
+
}
|
|
383
|
+
const linked = findLink();
|
|
384
|
+
if (!linked) throw new CliError("Nenhum app vinculado a esta pasta. Rode `hacli link` ou passe --app <nome>.");
|
|
385
|
+
if (linked.link.apiUrl !== apiUrl()) throw new CliError(`Esta pasta est\xE1 vinculada a ${linked.link.apiUrl}.`);
|
|
386
|
+
return { id: linked.link.appId, name: linked.link.appName };
|
|
387
|
+
}
|
|
388
|
+
async function appsCommand(print) {
|
|
389
|
+
const apps = await listApps();
|
|
390
|
+
if (!apps.length) return print("Nenhum app nesta organiza\xE7\xE3o.");
|
|
391
|
+
print(table(["NOME", "STATUS", "URL", "ID"], apps.map((app) => [app.name, statusColor(app.status ?? "\u2014"), appUrl(app) ?? "\u2014", color.dim(app.id)])));
|
|
392
|
+
}
|
|
393
|
+
async function linkCommand(print, target) {
|
|
394
|
+
if (!target) {
|
|
395
|
+
const apps = await listApps();
|
|
396
|
+
throw new CliError(`Informe o app: \`hacli link <nome>\`.
|
|
397
|
+
Apps: ${apps.map((a) => a.name).join(", ") || "nenhum"}`);
|
|
398
|
+
}
|
|
399
|
+
const app = await resolveApp(target);
|
|
400
|
+
const path = writeLink({ appId: app.id, appName: app.name, apiUrl: apiUrl() });
|
|
401
|
+
print(`${color.green("\u2713")} Pasta vinculada a ${color.bold(app.name)} (${path}).`);
|
|
402
|
+
}
|
|
403
|
+
async function statusCommand(print, appFlag) {
|
|
404
|
+
const target = await resolveApp(appFlag);
|
|
405
|
+
const [{ app }, { deployments }] = await Promise.all([
|
|
406
|
+
callTool("app_get", { appId: target.id }),
|
|
407
|
+
callTool("app_deployment_list", { appId: target.id })
|
|
408
|
+
]);
|
|
409
|
+
print(`${color.bold(app.name)} ${statusColor(app.status ?? "\u2014")}`);
|
|
410
|
+
if (appUrl(app)) print(`URL: ${appUrl(app)}`);
|
|
411
|
+
if (app.branch) print(`Branch: ${app.branch}`);
|
|
412
|
+
const last = deployments?.[0];
|
|
413
|
+
if (last) print(`\xDAltimo deploy: ${statusColor(last.status)}${last.phase ? ` (${last.phase})` : ""} \xB7 ${last.commitSha?.slice(0, 7) ?? "\u2014"} \xB7 ${last.createdAt ?? ""}`);
|
|
414
|
+
}
|
|
415
|
+
async function restartCommand(print, appFlag) {
|
|
416
|
+
const app = await resolveApp(appFlag);
|
|
417
|
+
await runMutation("app_restart", { appId: app.id });
|
|
418
|
+
print(`${color.green("\u2713")} ${app.name} reiniciando.`);
|
|
419
|
+
}
|
|
420
|
+
async function rollbackCommand(print, appFlag, deploymentId) {
|
|
421
|
+
const app = await resolveApp(appFlag);
|
|
422
|
+
let target = deploymentId;
|
|
423
|
+
if (!target) {
|
|
424
|
+
const { deployments } = await callTool("app_deployment_list", { appId: app.id });
|
|
425
|
+
const successes = (deployments ?? []).filter((d) => d.status === "success");
|
|
426
|
+
target = successes[1]?.id;
|
|
427
|
+
if (!target) throw new CliError("N\xE3o h\xE1 deploy anterior bem-sucedido para voltar. Informe o ID: `hacli rollback <deploymentId>`.");
|
|
428
|
+
}
|
|
429
|
+
await runMutation("app_rollback", { appId: app.id, deploymentId: target });
|
|
430
|
+
print(`${color.green("\u2713")} Rollback de ${app.name} para ${target} agendado.`);
|
|
431
|
+
}
|
|
432
|
+
async function openCommand(print, appFlag) {
|
|
433
|
+
const target = await resolveApp(appFlag);
|
|
434
|
+
const { app } = await callTool("app_get", { appId: target.id });
|
|
435
|
+
const url = appUrl(app);
|
|
436
|
+
if (!url) throw new CliError("O app ainda n\xE3o tem endere\xE7o p\xFAblico.");
|
|
437
|
+
print(url);
|
|
438
|
+
openBrowser(url);
|
|
439
|
+
}
|
|
440
|
+
async function previewsCommand(print, appFlag) {
|
|
441
|
+
const app = await resolveApp(appFlag);
|
|
442
|
+
const { previews } = await callTool("app_preview_list", { appId: app.id });
|
|
443
|
+
if (!previews?.length) return print("Nenhum preview aberto. Previews nascem de pull requests (plano Hobby em diante).");
|
|
444
|
+
print(table(["PR", "BRANCH", "ESTADO", "DEPLOY", "URL"], previews.map((p) => [
|
|
445
|
+
`#${p.prNumber}`,
|
|
446
|
+
p.headRef,
|
|
447
|
+
statusColor(p.state),
|
|
448
|
+
p.latestDeployment ? statusColor(p.latestDeployment.status) : "\u2014",
|
|
449
|
+
p.url ?? "\u2014"
|
|
450
|
+
])));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/deploy.ts
|
|
454
|
+
var TERMINAL = /* @__PURE__ */ new Set(["success", "failed", "cancelled"]);
|
|
455
|
+
var WAIT_TIMEOUT_MS = 60 * 6e4;
|
|
456
|
+
async function deployCommand(print, write, opts) {
|
|
457
|
+
const app = await resolveApp(opts.app);
|
|
458
|
+
const created = await runMutation("app_deploy", { appId: app.id, ...opts.noCache ? { options: { noCache: true } } : {} });
|
|
459
|
+
let deploymentId = created?.id;
|
|
460
|
+
if (!deploymentId) {
|
|
461
|
+
const { deployments } = await callTool("app_deployment_list", { appId: app.id });
|
|
462
|
+
deploymentId = deployments?.[0]?.id;
|
|
463
|
+
}
|
|
464
|
+
print(`${color.green("\u2713")} Deploy de ${color.bold(app.name)} agendado${deploymentId ? ` (${deploymentId})` : ""}.`);
|
|
465
|
+
if (created?.queuePosition) print(color.dim(`Posi\xE7\xE3o na fila: ${created.queuePosition}`));
|
|
466
|
+
if (!opts.wait) return;
|
|
467
|
+
if (!deploymentId) throw new CliError("N\xE3o foi poss\xEDvel identificar o deploy para acompanhar.");
|
|
468
|
+
const status = await followDeployment(app.id, deploymentId, write);
|
|
469
|
+
print("");
|
|
470
|
+
print(`Deploy ${statusColor(status)}.`);
|
|
471
|
+
if (status !== "success") throw new CliError(`O deploy terminou como ${status}.`, 1);
|
|
472
|
+
}
|
|
473
|
+
async function followDeployment(appId, deploymentId, write) {
|
|
474
|
+
const deadline = Date.now() + WAIT_TIMEOUT_MS;
|
|
475
|
+
let finalStatus = null;
|
|
476
|
+
let forceRefresh = false;
|
|
477
|
+
while (!finalStatus && Date.now() < deadline) {
|
|
478
|
+
try {
|
|
479
|
+
await streamEvents(`/cli/apps/${encodeURIComponent(appId)}/deployments/${encodeURIComponent(deploymentId)}/logs/stream`, (event) => {
|
|
480
|
+
if (event.type === "update" || event.type === "reset") write(String(event.text ?? ""));
|
|
481
|
+
if (event.type === "done") {
|
|
482
|
+
finalStatus = String(event.status);
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (event.type === "error") throw new CliError(String(event.message ?? "Erro no stream de logs."));
|
|
486
|
+
}, void 0, { forceRefresh });
|
|
487
|
+
forceRefresh = false;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error instanceof StreamTokenExpired) {
|
|
490
|
+
forceRefresh = true;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (error instanceof CliError) throw error;
|
|
494
|
+
}
|
|
495
|
+
if (finalStatus) break;
|
|
496
|
+
const { deployments } = await callTool("app_deployment_list", { appId });
|
|
497
|
+
const current = deployments?.find((d) => d.id === deploymentId);
|
|
498
|
+
if (current && TERMINAL.has(current.status)) finalStatus = current.status;
|
|
499
|
+
else await new Promise((r) => setTimeout(r, 3e3));
|
|
500
|
+
}
|
|
501
|
+
if (!finalStatus) throw new CliError("Tempo de espera esgotado (60 min).");
|
|
502
|
+
return finalStatus;
|
|
503
|
+
}
|
|
504
|
+
function newLines(previous, current) {
|
|
505
|
+
for (let overlap = Math.min(previous.length, current.length); overlap > 0; overlap--) {
|
|
506
|
+
let same = true;
|
|
507
|
+
for (let i = 0; i < overlap; i++) if (previous[previous.length - overlap + i] !== current[i]) {
|
|
508
|
+
same = false;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
if (same) return current.slice(overlap);
|
|
512
|
+
}
|
|
513
|
+
return current;
|
|
514
|
+
}
|
|
515
|
+
async function logsCommand(print, opts) {
|
|
516
|
+
const app = await resolveApp(opts.app);
|
|
517
|
+
if (opts.deployment) {
|
|
518
|
+
if (opts.follow) {
|
|
519
|
+
await followDeployment(app.id, opts.deployment, (text) => process.stdout.write(text));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const { logs } = await callTool("app_deployment_logs", { appId: app.id, deploymentId: opts.deployment });
|
|
523
|
+
return print(logs);
|
|
524
|
+
}
|
|
525
|
+
if (!opts.follow) {
|
|
526
|
+
const { logs } = await callTool("app_logs", { appId: app.id, lines: opts.lines ?? 200 });
|
|
527
|
+
return print((logs ?? []).join("\n"));
|
|
528
|
+
}
|
|
529
|
+
let forceRefresh = false, seenInitial = false;
|
|
530
|
+
const recent = [];
|
|
531
|
+
const show = (lines) => {
|
|
532
|
+
for (const line of lines) {
|
|
533
|
+
print(line);
|
|
534
|
+
recent.push(line);
|
|
535
|
+
}
|
|
536
|
+
if (recent.length > 500) recent.splice(0, recent.length - 500);
|
|
537
|
+
};
|
|
538
|
+
for (; ; ) {
|
|
539
|
+
try {
|
|
540
|
+
await streamEvents(`/cli/apps/${encodeURIComponent(app.id)}/logs/stream`, (event) => {
|
|
541
|
+
const lines = event.lines ?? [];
|
|
542
|
+
if (event.type === "initial") {
|
|
543
|
+
show(seenInitial ? newLines(recent, lines) : lines);
|
|
544
|
+
seenInitial = true;
|
|
545
|
+
}
|
|
546
|
+
if (event.type === "update") show(lines);
|
|
547
|
+
if (event.type === "timeout") print(color.dim("\u2014 stream encerrado ap\xF3s 30 min; rode de novo para continuar \u2014"));
|
|
548
|
+
if (event.type === "error") throw new CliError(String(event.message ?? "Erro no stream de logs."));
|
|
549
|
+
}, void 0, { forceRefresh });
|
|
550
|
+
return;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof StreamTokenExpired) {
|
|
553
|
+
forceRefresh = true;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/commands/env.ts
|
|
562
|
+
async function envCommand(print, sub, args, appFlag) {
|
|
563
|
+
const app = await resolveApp(appFlag);
|
|
564
|
+
if (!sub || sub === "ls" || sub === "list") {
|
|
565
|
+
const { envs } = await callTool("app_env_list", { appId: app.id });
|
|
566
|
+
if (!envs?.length) return print("Nenhuma vari\xE1vel definida.");
|
|
567
|
+
return print(table(["CHAVE", "VALOR", "ESCOPO"], envs.map((env) => [env.key, color.dim(env.value), env.scope ?? "both"])));
|
|
568
|
+
}
|
|
569
|
+
if (sub === "set") {
|
|
570
|
+
if (!args.length) throw new CliError("Uso: hacli env set CHAVE=valor [OUTRA=valor ...]");
|
|
571
|
+
const { envs } = await callTool("app_env_list", { appId: app.id });
|
|
572
|
+
for (const pair of args) {
|
|
573
|
+
const eq = pair.indexOf("=");
|
|
574
|
+
if (eq <= 0) throw new CliError(`Formato inv\xE1lido: "${pair}". Use CHAVE=valor.`);
|
|
575
|
+
const key = pair.slice(0, eq), value = pair.slice(eq + 1);
|
|
576
|
+
const existing = envs?.find((env) => env.key === key);
|
|
577
|
+
await runMutation("app_env_set", existing ? { appId: app.id, envId: existing.id, value } : { appId: app.id, key, value });
|
|
578
|
+
print(`${color.green("\u2713")} ${key} ${existing ? "atualizada" : "criada"}.`);
|
|
579
|
+
}
|
|
580
|
+
return print(color.dim("As vari\xE1veis valem a partir do pr\xF3ximo deploy (`hacli deploy`)."));
|
|
581
|
+
}
|
|
582
|
+
if (sub === "unset" || sub === "rm") {
|
|
583
|
+
if (!args.length) throw new CliError("Uso: hacli env unset CHAVE [OUTRA ...]");
|
|
584
|
+
const { envs } = await callTool("app_env_list", { appId: app.id });
|
|
585
|
+
for (const key of args) {
|
|
586
|
+
const existing = envs?.find((env) => env.key === key);
|
|
587
|
+
if (!existing) {
|
|
588
|
+
print(color.yellow(`\u2022 ${key} n\xE3o existe; nada a remover.`));
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
await runMutation("app_env_delete", { appId: app.id, envId: existing.id });
|
|
592
|
+
print(`${color.green("\u2713")} ${key} removida.`);
|
|
593
|
+
}
|
|
594
|
+
return print(color.dim("A remo\xE7\xE3o vale a partir do pr\xF3ximo deploy."));
|
|
595
|
+
}
|
|
596
|
+
throw new CliError(`Subcomando desconhecido: env ${sub}. Use ls, set ou unset.`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/index.ts
|
|
600
|
+
var VERSION = "0.1.0";
|
|
601
|
+
var HELP = `${color.bold("hacli")} \u2014 CLI da Hospede Aqui
|
|
602
|
+
|
|
603
|
+
Uso: hacli <comando> [op\xE7\xF5es]
|
|
604
|
+
|
|
605
|
+
Conta
|
|
606
|
+
login Autoriza a CLI no navegador
|
|
607
|
+
logout Revoga e apaga o login salvo
|
|
608
|
+
whoami Mostra usu\xE1rio, organiza\xE7\xE3o e plano
|
|
609
|
+
|
|
610
|
+
Apps
|
|
611
|
+
apps Lista os apps da organiza\xE7\xE3o
|
|
612
|
+
link <app> Vincula a pasta atual a um app (.hospedeaqui.json)
|
|
613
|
+
status Status do app e do \xFAltimo deploy
|
|
614
|
+
deploy [--wait] [--no-cache] Agenda um deploy (--wait acompanha e falha se o deploy falhar)
|
|
615
|
+
logs [-f] [-n 200] [--deployment <id>] Logs de runtime (ou de um deploy)
|
|
616
|
+
env ls | set K=v ... | unset K ... Vari\xE1veis do app (valores mascarados)
|
|
617
|
+
rollback [deploymentId] Volta para um deploy anterior
|
|
618
|
+
restart Reinicia o app
|
|
619
|
+
open Abre o app no navegador
|
|
620
|
+
previews Previews por pull request
|
|
621
|
+
|
|
622
|
+
Op\xE7\xF5es gerais
|
|
623
|
+
--app <nome|id> App alvo (sem link na pasta)
|
|
624
|
+
-h, --help | -v, --version
|
|
625
|
+
|
|
626
|
+
Em CI, defina HOSPEDEAQUI_TOKEN com um token de acesso (Integra\xE7\xF5es \u203A MCP \u203A Tokens).
|
|
627
|
+
Documenta\xE7\xE3o: https://hospedeaqui.cloud/cli`;
|
|
628
|
+
async function main(argv) {
|
|
629
|
+
const { values, positionals } = parseArgs({
|
|
630
|
+
args: argv,
|
|
631
|
+
allowPositionals: true,
|
|
632
|
+
strict: true,
|
|
633
|
+
options: {
|
|
634
|
+
app: { type: "string" },
|
|
635
|
+
wait: { type: "boolean" },
|
|
636
|
+
"no-cache": { type: "boolean" },
|
|
637
|
+
follow: { type: "boolean", short: "f" },
|
|
638
|
+
lines: { type: "string", short: "n" },
|
|
639
|
+
deployment: { type: "string" },
|
|
640
|
+
help: { type: "boolean", short: "h" },
|
|
641
|
+
version: { type: "boolean", short: "v" }
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
const [command, ...rest] = positionals;
|
|
645
|
+
const print = (line) => process.stdout.write(line + "\n");
|
|
646
|
+
if (values.version) return print(VERSION);
|
|
647
|
+
if (values.help || !command || command === "help") return print(HELP);
|
|
648
|
+
switch (command) {
|
|
649
|
+
case "login":
|
|
650
|
+
await login(print);
|
|
651
|
+
return print(`${color.green("\u2713")} Login feito em ${apiUrl()}.`);
|
|
652
|
+
case "logout": {
|
|
653
|
+
const result = await logout();
|
|
654
|
+
if (result === "none") return print("Nenhum login salvo.");
|
|
655
|
+
return print(result === "revoked" ? `${color.green("\u2713")} Login removido e sess\xE3o revogada.` : `${color.yellow("!")} Login removido deste computador, mas o servidor n\xE3o confirmou a revoga\xE7\xE3o. Revogue em Integra\xE7\xF5es \u203A MCP.`);
|
|
656
|
+
}
|
|
657
|
+
case "whoami": {
|
|
658
|
+
const ctx = await callTool("platform_get_context");
|
|
659
|
+
return print(`${ctx.user?.name ?? "\u2014"} <${ctx.user?.email ?? "\u2014"}>
|
|
660
|
+
Organiza\xE7\xE3o: ${ctx.organization?.name ?? "\u2014"}
|
|
661
|
+
Plano: ${ctx.plan?.name ?? "sem plano"}`);
|
|
662
|
+
}
|
|
663
|
+
case "apps":
|
|
664
|
+
return appsCommand(print);
|
|
665
|
+
case "link":
|
|
666
|
+
return linkCommand(print, rest[0]);
|
|
667
|
+
case "status":
|
|
668
|
+
return statusCommand(print, values.app);
|
|
669
|
+
case "deploy":
|
|
670
|
+
return deployCommand(print, (text) => process.stdout.write(text), { app: values.app, wait: values.wait, noCache: values["no-cache"] });
|
|
671
|
+
case "logs":
|
|
672
|
+
return logsCommand(print, {
|
|
673
|
+
app: values.app,
|
|
674
|
+
follow: values.follow,
|
|
675
|
+
deployment: values.deployment,
|
|
676
|
+
lines: values.lines ? Math.min(1e3, Math.max(1, Number(values.lines) || 200)) : void 0
|
|
677
|
+
});
|
|
678
|
+
case "env":
|
|
679
|
+
return envCommand(print, rest[0], rest.slice(1), values.app);
|
|
680
|
+
case "rollback":
|
|
681
|
+
return rollbackCommand(print, values.app, rest[0]);
|
|
682
|
+
case "restart":
|
|
683
|
+
return restartCommand(print, values.app);
|
|
684
|
+
case "open":
|
|
685
|
+
return openCommand(print, values.app);
|
|
686
|
+
case "previews":
|
|
687
|
+
return previewsCommand(print, values.app);
|
|
688
|
+
default:
|
|
689
|
+
throw new CliError(`Comando desconhecido: ${command}. Veja \`hacli --help\`.`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/bin.ts
|
|
694
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
695
|
+
if (error instanceof CliError) {
|
|
696
|
+
process.stderr.write(color.red(`\u2717 ${error.message}`) + "\n");
|
|
697
|
+
process.exit(error.exitCode);
|
|
698
|
+
}
|
|
699
|
+
const code = error.code;
|
|
700
|
+
if (code?.startsWith("ERR_PARSE_ARGS")) {
|
|
701
|
+
process.stderr.write(color.red(`\u2717 ${error.message}`) + "\n");
|
|
702
|
+
process.exit(2);
|
|
703
|
+
}
|
|
704
|
+
process.stderr.write(color.red(`\u2717 Erro inesperado: ${error instanceof Error ? error.message : String(error)}`) + "\n");
|
|
705
|
+
process.exit(1);
|
|
706
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hacloud/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "hacli, a CLI da Hospede Aqui: deploy, logs, variáveis e previews dos seus apps pelo terminal.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"hacli": "dist/hacli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "esbuild src/bin.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/hacli.mjs --banner:js=\"#!/usr/bin/env node\"",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "tsx --test \"test/**/*.test.ts\"",
|
|
21
|
+
"dev": "tsx src/bin.ts"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"esbuild": "^0.25.12",
|
|
26
|
+
"tsx": "^4.19.0",
|
|
27
|
+
"typescript": "^5.7.0"
|
|
28
|
+
},
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://hospedeaqui.cloud/cli",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"hospede aqui",
|
|
36
|
+
"hacli",
|
|
37
|
+
"deploy",
|
|
38
|
+
"cli",
|
|
39
|
+
"paas"
|
|
40
|
+
]
|
|
41
|
+
}
|