onveloz 0.0.0-beta.1
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/dist/index.mjs +2949 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2949 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "@commander-js/extra-typings";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { exec, execSync } from "node:child_process";
|
|
5
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join, relative, resolve } from "node:path";
|
|
9
|
+
import * as readline from "node:readline";
|
|
10
|
+
import { createInterface } from "node:readline";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
import { createAuthClient } from "better-auth/client";
|
|
13
|
+
import { deviceAuthorizationClient } from "better-auth/client/plugins";
|
|
14
|
+
import { createORPCClient } from "@orpc/client";
|
|
15
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
18
|
+
import ignore from "ignore";
|
|
19
|
+
import tar from "tar";
|
|
20
|
+
|
|
21
|
+
//#region src/lib/config.ts
|
|
22
|
+
const CONFIG_DIR = join(homedir(), ".veloz");
|
|
23
|
+
const ENV_API_URL = process.env.VELOZ_API_URL;
|
|
24
|
+
const DEFAULT_API_URL = ENV_API_URL ?? "https://platform.onveloz.com";
|
|
25
|
+
function getConfigFile() {
|
|
26
|
+
if (!ENV_API_URL) return join(CONFIG_DIR, "config.json");
|
|
27
|
+
return join(CONFIG_DIR, `config.${createHash("md5").update(ENV_API_URL).digest("hex").slice(0, 8)}.json`);
|
|
28
|
+
}
|
|
29
|
+
const CONFIG_FILE$1 = getConfigFile();
|
|
30
|
+
function ensureConfigDir() {
|
|
31
|
+
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
function loadConfig$1() {
|
|
34
|
+
if (!existsSync(CONFIG_FILE$1)) return {
|
|
35
|
+
apiKey: "",
|
|
36
|
+
apiUrl: DEFAULT_API_URL
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(CONFIG_FILE$1, "utf-8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
return {
|
|
42
|
+
apiKey: parsed.apiKey ?? "",
|
|
43
|
+
apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return {
|
|
47
|
+
apiKey: "",
|
|
48
|
+
apiUrl: DEFAULT_API_URL
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function saveConfig$1(config) {
|
|
53
|
+
ensureConfigDir();
|
|
54
|
+
const existing = loadConfig$1();
|
|
55
|
+
const merged = {
|
|
56
|
+
apiKey: config.apiKey ?? existing.apiKey,
|
|
57
|
+
apiUrl: config.apiUrl ?? existing.apiUrl
|
|
58
|
+
};
|
|
59
|
+
writeFileSync(CONFIG_FILE$1, JSON.stringify(merged, null, 2), "utf-8");
|
|
60
|
+
}
|
|
61
|
+
function deleteConfig() {
|
|
62
|
+
if (existsSync(CONFIG_FILE$1)) unlinkSync(CONFIG_FILE$1);
|
|
63
|
+
}
|
|
64
|
+
function isAuthenticated() {
|
|
65
|
+
return loadConfig$1().apiKey.length > 0;
|
|
66
|
+
}
|
|
67
|
+
let envApiUrlLogged = false;
|
|
68
|
+
async function requireAuth() {
|
|
69
|
+
let config = loadConfig$1();
|
|
70
|
+
if (!config.apiKey) {
|
|
71
|
+
await performLogin(config.apiUrl);
|
|
72
|
+
config = loadConfig$1();
|
|
73
|
+
}
|
|
74
|
+
if (ENV_API_URL && !envApiUrlLogged) {
|
|
75
|
+
envApiUrlLogged = true;
|
|
76
|
+
console.log(`\n🔧 Usando API: ${ENV_API_URL}\n`);
|
|
77
|
+
}
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/lib/utils.ts
|
|
83
|
+
function isRateLimitError(error) {
|
|
84
|
+
if (!(error instanceof Error)) return null;
|
|
85
|
+
const orpcError = error;
|
|
86
|
+
if (orpcError.code === "TOO_MANY_REQUESTS" && orpcError.data?.retryAfterMs) return { retryAfterMs: orpcError.data.retryAfterMs };
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function handleError(error) {
|
|
90
|
+
if (error instanceof Error) {
|
|
91
|
+
const orpcError = error;
|
|
92
|
+
if (orpcError.code) {
|
|
93
|
+
const message = orpcError.data?.message ?? orpcError.message ?? {
|
|
94
|
+
NOT_FOUND: "Recurso não encontrado.",
|
|
95
|
+
FORBIDDEN: "Sem permissão para acessar este recurso.",
|
|
96
|
+
CONFLICT: "Conflito — recurso já existe.",
|
|
97
|
+
BAD_REQUEST: "Requisição inválida.",
|
|
98
|
+
UNAUTHORIZED: "Não autorizado. Execute `veloz login` para autenticar.",
|
|
99
|
+
INTERNAL_SERVER_ERROR: "Erro interno do servidor. Tente novamente.",
|
|
100
|
+
TOO_MANY_REQUESTS: "Tente novamente em alguns segundos."
|
|
101
|
+
}[orpcError.code] ?? "Erro desconhecido.";
|
|
102
|
+
console.error(chalk.red(`\n✗ Erro: ${message}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (error.message.includes("fetch") || error.message.includes("ECONNREFUSED")) {
|
|
106
|
+
console.error(chalk.red("\n✗ Erro de conexão: Não foi possível conectar ao servidor Veloz."));
|
|
107
|
+
console.error(chalk.yellow(" Verifique se o servidor está rodando e a URL está correta."));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
console.error(chalk.red(`\n✗ Erro: ${error.message}`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.error(chalk.red("\n✗ Erro inesperado."));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
function success(message) {
|
|
117
|
+
console.log(chalk.green(`\n✓ ${message}`));
|
|
118
|
+
}
|
|
119
|
+
function info(message) {
|
|
120
|
+
console.log(chalk.cyan(`ℹ ${message}`));
|
|
121
|
+
}
|
|
122
|
+
function warn(message) {
|
|
123
|
+
console.log(chalk.yellow(`⚠ ${message}`));
|
|
124
|
+
}
|
|
125
|
+
function spinner(text) {
|
|
126
|
+
return ora({
|
|
127
|
+
text,
|
|
128
|
+
color: "cyan"
|
|
129
|
+
}).start();
|
|
130
|
+
}
|
|
131
|
+
function printTable(headers, rows) {
|
|
132
|
+
const colWidths = headers.map((h, i) => {
|
|
133
|
+
const maxRow = rows.reduce((max, row) => Math.max(max, (row[i] ?? "").length), 0);
|
|
134
|
+
return Math.max(h.length, maxRow);
|
|
135
|
+
});
|
|
136
|
+
const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]))).join(" ");
|
|
137
|
+
const separator = colWidths.map((w) => "─".repeat(w)).join("──");
|
|
138
|
+
console.log(`\n${headerLine}`);
|
|
139
|
+
console.log(chalk.dim(separator));
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
|
|
142
|
+
console.log(line);
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
async function prompt(question) {
|
|
147
|
+
const rl = createInterface({
|
|
148
|
+
input: process.stdin,
|
|
149
|
+
output: process.stdout
|
|
150
|
+
});
|
|
151
|
+
return new Promise((resolve$1) => {
|
|
152
|
+
rl.question(chalk.cyan(`${question} `), (answer) => {
|
|
153
|
+
rl.close();
|
|
154
|
+
resolve$1(answer.trim());
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async function promptConfirm(question, defaultYes = true) {
|
|
159
|
+
const answer = await prompt(`${question} ${defaultYes ? "(S/n)" : "(s/N)"}`);
|
|
160
|
+
if (answer === "") return defaultYes;
|
|
161
|
+
return answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y");
|
|
162
|
+
}
|
|
163
|
+
async function promptSelect(question, options) {
|
|
164
|
+
console.log(chalk.cyan(`\n${question}\n`));
|
|
165
|
+
for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
|
|
166
|
+
const answer = await prompt("\nEscolha (número):");
|
|
167
|
+
const index = parseInt(answer, 10) - 1;
|
|
168
|
+
if (isNaN(index) || index < 0 || index >= options.length) {
|
|
169
|
+
console.error(chalk.red("Opção inválida."));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
return options[index].value;
|
|
173
|
+
}
|
|
174
|
+
async function promptMultiSelect(question, options) {
|
|
175
|
+
console.log(chalk.cyan(`\n${question}\n`));
|
|
176
|
+
for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
|
|
177
|
+
console.log(chalk.dim(`\n * = todos`));
|
|
178
|
+
const answer = await prompt("\nEscolha (números separados por vírgula, ou *):");
|
|
179
|
+
if (answer.trim() === "*") return options.map((o) => o.value);
|
|
180
|
+
const indices = answer.split(",").map((s) => parseInt(s.trim(), 10) - 1).filter((i) => !isNaN(i) && i >= 0 && i < options.length);
|
|
181
|
+
if (indices.length === 0) {
|
|
182
|
+
console.error(chalk.red("Nenhuma opção válida selecionada."));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
return indices.map((i) => options[i].value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/commands/login.ts
|
|
190
|
+
function openBrowser(url) {
|
|
191
|
+
const os = platform();
|
|
192
|
+
exec(os === "darwin" ? `open "${url}"` : os === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`, () => {});
|
|
193
|
+
}
|
|
194
|
+
const CLIENT_ID = "veloz-cli";
|
|
195
|
+
async function performLogin(apiUrl) {
|
|
196
|
+
const authClient = createAuthClient({
|
|
197
|
+
baseURL: apiUrl,
|
|
198
|
+
plugins: [deviceAuthorizationClient()]
|
|
199
|
+
});
|
|
200
|
+
const s = spinner("Iniciando autenticação...");
|
|
201
|
+
try {
|
|
202
|
+
const { data, error } = await authClient.device.code({ client_id: CLIENT_ID });
|
|
203
|
+
if (error || !data) {
|
|
204
|
+
s.fail("Erro ao iniciar autenticação.");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
s.stop();
|
|
208
|
+
const verificationUrl = `${apiUrl.includes("localhost") ? "http://localhost:3001" : "https://app.onveloz.com"}${data.verification_uri_complete ? new URL(data.verification_uri_complete).pathname + new URL(data.verification_uri_complete).search : `/cli/auth?code=${data.user_code}`}`;
|
|
209
|
+
console.log();
|
|
210
|
+
info("Abrindo navegador para autenticação...");
|
|
211
|
+
console.log();
|
|
212
|
+
console.log(chalk.white(` Código de verificação: ${chalk.bold.cyan(data.user_code)}`));
|
|
213
|
+
console.log();
|
|
214
|
+
console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
|
|
215
|
+
console.log(chalk.dim(` ${verificationUrl}`));
|
|
216
|
+
console.log();
|
|
217
|
+
openBrowser(verificationUrl);
|
|
218
|
+
const pollSpinner = spinner("Aguardando autorização no navegador...");
|
|
219
|
+
const token = await pollForToken(authClient, data.device_code, data.interval || 5);
|
|
220
|
+
if (!token) {
|
|
221
|
+
pollSpinner.fail("Tempo esgotado. Execute `veloz login` para tentar novamente.");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
pollSpinner.stop();
|
|
225
|
+
saveConfig$1({
|
|
226
|
+
apiKey: token,
|
|
227
|
+
apiUrl
|
|
228
|
+
});
|
|
229
|
+
success("Autenticado com sucesso!");
|
|
230
|
+
} catch (err) {
|
|
231
|
+
s.stop();
|
|
232
|
+
if (err instanceof Error && (err.message.includes("fetch") || err.message.includes("ECONNREFUSED"))) {
|
|
233
|
+
console.error(chalk.red("\n✗ Erro de conexão: Não foi possível conectar ao servidor Veloz."));
|
|
234
|
+
console.error(chalk.yellow(" Verifique se o servidor está rodando e a URL está correta."));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
console.error(chalk.red(`\n✗ Erro: ${err instanceof Error ? err.message : "Erro inesperado."}`));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const loginCommand = new Command("login").description("Autenticar na plataforma Veloz").option("--api-url <url>", "URL da API Veloz").option("--api-key <key>", "Chave de API (para CI/automação)").action(async (opts) => {
|
|
242
|
+
if (isAuthenticated()) {
|
|
243
|
+
warn("Você já está autenticado.");
|
|
244
|
+
if ((await prompt("Deseja fazer login novamente? (s/N)")).toLowerCase() !== "s") process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
const config = loadConfig$1();
|
|
247
|
+
const apiUrl = opts.apiUrl ?? config.apiUrl;
|
|
248
|
+
if (opts.apiKey) {
|
|
249
|
+
saveConfig$1({
|
|
250
|
+
apiKey: opts.apiKey,
|
|
251
|
+
apiUrl
|
|
252
|
+
});
|
|
253
|
+
success("Autenticado com sucesso!");
|
|
254
|
+
info("Configuração salva em ~/.veloz/config.json");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
await performLogin(apiUrl);
|
|
258
|
+
});
|
|
259
|
+
async function pollForToken(authClient, deviceCode, interval) {
|
|
260
|
+
let pollingInterval = interval;
|
|
261
|
+
const maxAttempts = Math.ceil(300 / pollingInterval);
|
|
262
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
263
|
+
await new Promise((r) => setTimeout(r, pollingInterval * 1e3));
|
|
264
|
+
try {
|
|
265
|
+
const { data, error } = await authClient.device.token({
|
|
266
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
267
|
+
device_code: deviceCode,
|
|
268
|
+
client_id: CLIENT_ID
|
|
269
|
+
});
|
|
270
|
+
if (data?.access_token) return data.access_token;
|
|
271
|
+
if (error?.error === "slow_down") pollingInterval += 5;
|
|
272
|
+
if (error?.error === "expired_token") return null;
|
|
273
|
+
} catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const logoutCommand = new Command("logout").description("Encerrar sessão na plataforma Veloz").action(() => {
|
|
280
|
+
if (!isAuthenticated()) {
|
|
281
|
+
warn("Você não está autenticado.");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
deleteConfig();
|
|
285
|
+
success("Sessão encerrada com sucesso.");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region ../../packages/api/src/client.ts
|
|
290
|
+
function createClient(baseUrl, headers) {
|
|
291
|
+
return createORPCClient(new RPCLink({
|
|
292
|
+
url: `${baseUrl}/rpc`,
|
|
293
|
+
headers: headers ?? (() => ({})),
|
|
294
|
+
fetch: (request, init) => {
|
|
295
|
+
return globalThis.fetch(request, {
|
|
296
|
+
...init,
|
|
297
|
+
credentials: "include"
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/lib/client.ts
|
|
305
|
+
async function getClient() {
|
|
306
|
+
const config = await requireAuth();
|
|
307
|
+
return createClient(config.apiUrl, () => ({ Authorization: `Bearer ${config.apiKey}` }));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/commands/projects.ts
|
|
312
|
+
const projectsCommand = new Command("projects").alias("projetos").description("Gerenciar projetos");
|
|
313
|
+
projectsCommand.command("list").alias("listar").description("Listar todos os projetos").action(async () => {
|
|
314
|
+
const spin = spinner("Carregando projetos...");
|
|
315
|
+
try {
|
|
316
|
+
const projects = await (await getClient()).projects.list();
|
|
317
|
+
spin.stop();
|
|
318
|
+
if (projects.length === 0) {
|
|
319
|
+
info("Nenhum projeto encontrado. Crie um pelo dashboard.");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
printTable([
|
|
323
|
+
"ID",
|
|
324
|
+
"Nome",
|
|
325
|
+
"Slug",
|
|
326
|
+
"Repo GitHub",
|
|
327
|
+
"Criado em"
|
|
328
|
+
], projects.map((p) => [
|
|
329
|
+
p.id.slice(0, 8),
|
|
330
|
+
p.name,
|
|
331
|
+
p.slug,
|
|
332
|
+
p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
|
|
333
|
+
new Date(p.createdAt).toLocaleDateString("pt-BR")
|
|
334
|
+
]));
|
|
335
|
+
} catch (error) {
|
|
336
|
+
spin.stop();
|
|
337
|
+
handleError(error);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region ../../packages/config/veloz-config.ts
|
|
343
|
+
const ServiceTypeSchema = z.enum(["web", "static"]);
|
|
344
|
+
const PackageManagerSchema = z.enum([
|
|
345
|
+
"npm",
|
|
346
|
+
"yarn",
|
|
347
|
+
"pnpm",
|
|
348
|
+
"bun",
|
|
349
|
+
"auto"
|
|
350
|
+
]);
|
|
351
|
+
const BuildConfigSchema = z.object({
|
|
352
|
+
command: z.string().nullable().optional(),
|
|
353
|
+
nodeVersion: z.string().regex(/^[0-9]+(\.x)?$/).default("20").optional(),
|
|
354
|
+
packageManager: PackageManagerSchema.default("auto").optional(),
|
|
355
|
+
installCommand: z.string().nullable().optional(),
|
|
356
|
+
outputDir: z.string().nullable().optional()
|
|
357
|
+
});
|
|
358
|
+
const RuntimeConfigSchema = z.object({
|
|
359
|
+
command: z.string().nullable().optional(),
|
|
360
|
+
port: z.number().min(1).max(65535).default(3e3).optional(),
|
|
361
|
+
healthCheck: z.object({
|
|
362
|
+
path: z.string().default("/").optional(),
|
|
363
|
+
interval: z.number().default(30).optional(),
|
|
364
|
+
timeout: z.number().default(10).optional()
|
|
365
|
+
}).optional()
|
|
366
|
+
});
|
|
367
|
+
const ResourcesSchema = z.object({
|
|
368
|
+
instances: z.number().min(1).max(10).default(1).optional(),
|
|
369
|
+
cpu: z.string().regex(/^[0-9]+(\.[0-9]+)?|[0-9]+m$/).default("500m").optional(),
|
|
370
|
+
memory: z.string().regex(/^[0-9]+(Mi|Gi)$/).default("512Mi").optional(),
|
|
371
|
+
autoscale: z.object({
|
|
372
|
+
enabled: z.boolean().default(false).optional(),
|
|
373
|
+
minInstances: z.number().min(1).default(1).optional(),
|
|
374
|
+
maxInstances: z.number().min(1).max(20).default(3).optional(),
|
|
375
|
+
targetCPU: z.number().min(10).max(90).default(70).optional()
|
|
376
|
+
}).optional()
|
|
377
|
+
});
|
|
378
|
+
const EnvVarDefinitionSchema = z.object({
|
|
379
|
+
description: z.string().optional(),
|
|
380
|
+
required: z.boolean().default(false).optional(),
|
|
381
|
+
example: z.string().optional()
|
|
382
|
+
});
|
|
383
|
+
const ServiceConfigSchema = z.object({
|
|
384
|
+
id: z.string(),
|
|
385
|
+
name: z.string(),
|
|
386
|
+
type: ServiceTypeSchema.default("web"),
|
|
387
|
+
root: z.string().default("."),
|
|
388
|
+
branch: z.string().optional(),
|
|
389
|
+
build: BuildConfigSchema.optional(),
|
|
390
|
+
runtime: RuntimeConfigSchema.optional(),
|
|
391
|
+
env: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
|
|
392
|
+
resources: ResourcesSchema.optional()
|
|
393
|
+
});
|
|
394
|
+
const ProjectConfigSchema = z.object({
|
|
395
|
+
id: z.string(),
|
|
396
|
+
name: z.string(),
|
|
397
|
+
slug: z.string().regex(/^[a-z0-9-]+$/).optional()
|
|
398
|
+
});
|
|
399
|
+
const ServiceDefaultsSchema = z.object({
|
|
400
|
+
type: ServiceTypeSchema.optional(),
|
|
401
|
+
branch: z.string().optional(),
|
|
402
|
+
build: BuildConfigSchema.optional(),
|
|
403
|
+
runtime: RuntimeConfigSchema.optional(),
|
|
404
|
+
resources: ResourcesSchema.optional()
|
|
405
|
+
});
|
|
406
|
+
const VelozConfigSchema = z.object({
|
|
407
|
+
$schema: z.string().optional(),
|
|
408
|
+
version: z.literal("1.0"),
|
|
409
|
+
project: ProjectConfigSchema,
|
|
410
|
+
services: z.record(z.string(), ServiceConfigSchema),
|
|
411
|
+
defaults: ServiceDefaultsSchema.optional(),
|
|
412
|
+
created: z.string().datetime().optional(),
|
|
413
|
+
updated: z.string().datetime().optional()
|
|
414
|
+
});
|
|
415
|
+
function mergeServiceWithDefaults(service, defaults) {
|
|
416
|
+
if (!defaults) return service;
|
|
417
|
+
return {
|
|
418
|
+
...service,
|
|
419
|
+
type: service.type ?? defaults.type ?? "web",
|
|
420
|
+
branch: service.branch ?? defaults.branch,
|
|
421
|
+
build: {
|
|
422
|
+
...defaults.build,
|
|
423
|
+
...service.build
|
|
424
|
+
},
|
|
425
|
+
runtime: {
|
|
426
|
+
...defaults.runtime,
|
|
427
|
+
...service.runtime
|
|
428
|
+
},
|
|
429
|
+
resources: {
|
|
430
|
+
...defaults.resources,
|
|
431
|
+
...service.resources
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function parseVelozConfig(data) {
|
|
436
|
+
return VelozConfigSchema.parse(data);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/lib/link.ts
|
|
441
|
+
const CONFIG_FILE = "veloz.json";
|
|
442
|
+
const LOCAL_CONFIG_FILE = "veloz.local.json";
|
|
443
|
+
/**
|
|
444
|
+
* Find the root directory (git root or monorepo root)
|
|
445
|
+
*/
|
|
446
|
+
function findProjectRoot(startPath = process.cwd()) {
|
|
447
|
+
let currentPath = resolve(startPath);
|
|
448
|
+
const root = resolve("/");
|
|
449
|
+
while (currentPath !== root) {
|
|
450
|
+
if (existsSync(join(currentPath, ".git"))) return currentPath;
|
|
451
|
+
if (existsSync(join(currentPath, "pnpm-workspace.yaml")) || existsSync(join(currentPath, "lerna.json")) || existsSync(join(currentPath, "rush.json")) || existsSync(join(currentPath, "nx.json"))) return currentPath;
|
|
452
|
+
const pkgJsonPath = join(currentPath, "package.json");
|
|
453
|
+
if (existsSync(pkgJsonPath)) try {
|
|
454
|
+
if (JSON.parse(readFileSync(pkgJsonPath, "utf-8")).workspaces) return currentPath;
|
|
455
|
+
} catch {}
|
|
456
|
+
const parentPath = resolve(currentPath, "..");
|
|
457
|
+
if (parentPath === currentPath) break;
|
|
458
|
+
currentPath = parentPath;
|
|
459
|
+
}
|
|
460
|
+
return process.cwd();
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Get the config file name (veloz.local.json when VELOZ_API_URL is set, veloz.json otherwise)
|
|
464
|
+
*/
|
|
465
|
+
function getConfigFileName() {
|
|
466
|
+
return process.env.VELOZ_API_URL ? LOCAL_CONFIG_FILE : CONFIG_FILE;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get the path to veloz.json (or veloz.local.json when using custom API URL)
|
|
470
|
+
*/
|
|
471
|
+
function getConfigPath() {
|
|
472
|
+
return join(findProjectRoot(), getConfigFileName());
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Load the veloz.json config from project root
|
|
476
|
+
*/
|
|
477
|
+
function loadConfig() {
|
|
478
|
+
const path = getConfigPath();
|
|
479
|
+
if (!existsSync(path)) return null;
|
|
480
|
+
try {
|
|
481
|
+
const raw = readFileSync(path, "utf-8");
|
|
482
|
+
return parseVelozConfig(JSON.parse(raw));
|
|
483
|
+
} catch (error) {
|
|
484
|
+
console.error(`Error loading ${getConfigFileName()}:`, error);
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Save the veloz.json config to project root
|
|
490
|
+
*/
|
|
491
|
+
function saveConfig(config) {
|
|
492
|
+
const path = getConfigPath();
|
|
493
|
+
const configWithSchema = {
|
|
494
|
+
$schema: "https://veloz.app/schemas/veloz-config.schema.json",
|
|
495
|
+
...config
|
|
496
|
+
};
|
|
497
|
+
writeFileSync(path, JSON.stringify(configWithSchema, null, 2), "utf-8");
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Require config to exist, throw if not found
|
|
501
|
+
*/
|
|
502
|
+
function requireConfig() {
|
|
503
|
+
const config = loadConfig();
|
|
504
|
+
if (!config) throw new Error(`No ${getConfigFileName()} found in project root. Run 'veloz init' or 'veloz deploy' to set up your project.`);
|
|
505
|
+
return config;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Check if current directory is a git repository
|
|
509
|
+
*/
|
|
510
|
+
function isGitRepo() {
|
|
511
|
+
try {
|
|
512
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
513
|
+
return true;
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Get git remote origin info
|
|
520
|
+
*/
|
|
521
|
+
function getGitRemote() {
|
|
522
|
+
try {
|
|
523
|
+
const url = execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
|
|
524
|
+
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
525
|
+
if (httpsMatch) return {
|
|
526
|
+
owner: httpsMatch[1],
|
|
527
|
+
repo: httpsMatch[2]
|
|
528
|
+
};
|
|
529
|
+
const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/);
|
|
530
|
+
if (sshMatch) return {
|
|
531
|
+
owner: sshMatch[1],
|
|
532
|
+
repo: sshMatch[2]
|
|
533
|
+
};
|
|
534
|
+
return null;
|
|
535
|
+
} catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get current git branch
|
|
541
|
+
*/
|
|
542
|
+
function getGitBranch() {
|
|
543
|
+
try {
|
|
544
|
+
return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
|
|
545
|
+
} catch {
|
|
546
|
+
return "main";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/commands/link.ts
|
|
552
|
+
const linkCommand = new Command("link").description("Verificar vínculo do projeto com Veloz").action(async () => {
|
|
553
|
+
const config = loadConfig();
|
|
554
|
+
if (!config) {
|
|
555
|
+
warn("Nenhum projeto configurado. Execute 'veloz deploy' para configurar seu projeto.");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
console.log(chalk.bold("\n🔗 Projeto Vinculado\n"));
|
|
559
|
+
console.log(` ${chalk.bold("Projeto:")} ${chalk.cyan(config.project.name)}`);
|
|
560
|
+
console.log(` ${chalk.bold("ID:")} ${chalk.dim(config.project.id)}`);
|
|
561
|
+
const services = Object.entries(config.services);
|
|
562
|
+
if (services.length > 0) {
|
|
563
|
+
console.log(`\n ${chalk.bold("Serviços:")}`);
|
|
564
|
+
services.forEach(([key, service]) => {
|
|
565
|
+
const type = service.type === "web" ? "Serviço Web" : "Site Estático";
|
|
566
|
+
console.log(` • ${chalk.cyan(service.name)} (${key}) - ${type}`);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
console.log();
|
|
570
|
+
info("Use 'veloz deploy' para fazer deploy ou atualizar serviços.");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
//#endregion
|
|
574
|
+
//#region ../../packages/api/src/lib/framework-detector.ts
|
|
575
|
+
const FRAMEWORK_RULES = [
|
|
576
|
+
{
|
|
577
|
+
name: "nextjs",
|
|
578
|
+
label: "Next.js",
|
|
579
|
+
type: "WEB",
|
|
580
|
+
match: (deps) => "next" in deps,
|
|
581
|
+
buildCommand: "npm run build",
|
|
582
|
+
startCommand: "npm start",
|
|
583
|
+
outputDir: ".next",
|
|
584
|
+
port: 3e3
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: "hono",
|
|
588
|
+
label: "Hono",
|
|
589
|
+
type: "WEB",
|
|
590
|
+
match: (deps) => "hono" in deps,
|
|
591
|
+
buildCommand: "npm run build",
|
|
592
|
+
startCommand: "npm start",
|
|
593
|
+
outputDir: "dist",
|
|
594
|
+
port: 3e3
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: "nuxt",
|
|
598
|
+
label: "Nuxt",
|
|
599
|
+
type: "WEB",
|
|
600
|
+
match: (deps) => "nuxt" in deps,
|
|
601
|
+
buildCommand: "npm run build",
|
|
602
|
+
startCommand: "npm start",
|
|
603
|
+
outputDir: ".nuxt",
|
|
604
|
+
port: 3e3
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "remix",
|
|
608
|
+
label: "Remix",
|
|
609
|
+
type: "WEB",
|
|
610
|
+
match: (deps) => "@remix-run/node" in deps,
|
|
611
|
+
buildCommand: "npm run build",
|
|
612
|
+
startCommand: "npm start",
|
|
613
|
+
outputDir: "build",
|
|
614
|
+
port: 3e3
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "sveltekit",
|
|
618
|
+
label: "SvelteKit",
|
|
619
|
+
type: "WEB",
|
|
620
|
+
match: (deps) => "@sveltejs/kit" in deps,
|
|
621
|
+
buildCommand: "npm run build",
|
|
622
|
+
startCommand: "npm start",
|
|
623
|
+
outputDir: ".svelte-kit",
|
|
624
|
+
port: 3e3
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: "astro",
|
|
628
|
+
label: "Astro",
|
|
629
|
+
type: "STATIC",
|
|
630
|
+
match: (deps) => "astro" in deps,
|
|
631
|
+
buildCommand: "npm run build",
|
|
632
|
+
startCommand: null,
|
|
633
|
+
outputDir: "dist",
|
|
634
|
+
port: 3e3
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "gatsby",
|
|
638
|
+
label: "Gatsby",
|
|
639
|
+
type: "STATIC",
|
|
640
|
+
match: (deps) => "gatsby" in deps,
|
|
641
|
+
buildCommand: "npm run build",
|
|
642
|
+
startCommand: null,
|
|
643
|
+
outputDir: "public",
|
|
644
|
+
port: 3e3
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "create-react-app",
|
|
648
|
+
label: "Create React App",
|
|
649
|
+
type: "STATIC",
|
|
650
|
+
match: (deps) => "react-scripts" in deps,
|
|
651
|
+
buildCommand: "npm run build",
|
|
652
|
+
startCommand: null,
|
|
653
|
+
outputDir: "build",
|
|
654
|
+
port: 3e3
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: "vite-react",
|
|
658
|
+
label: "Vite + React",
|
|
659
|
+
type: "STATIC",
|
|
660
|
+
match: (deps) => "vite" in deps && "react" in deps,
|
|
661
|
+
buildCommand: "npm run build",
|
|
662
|
+
startCommand: null,
|
|
663
|
+
outputDir: "dist",
|
|
664
|
+
port: 3e3
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "vite-vue",
|
|
668
|
+
label: "Vite + Vue",
|
|
669
|
+
type: "STATIC",
|
|
670
|
+
match: (deps) => "vite" in deps && "vue" in deps,
|
|
671
|
+
buildCommand: "npm run build",
|
|
672
|
+
startCommand: null,
|
|
673
|
+
outputDir: "dist",
|
|
674
|
+
port: 3e3
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "vite",
|
|
678
|
+
label: "Vite",
|
|
679
|
+
type: "STATIC",
|
|
680
|
+
match: (deps) => "vite" in deps,
|
|
681
|
+
buildCommand: "npm run build",
|
|
682
|
+
startCommand: null,
|
|
683
|
+
outputDir: "dist",
|
|
684
|
+
port: 3e3
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: "angular",
|
|
688
|
+
label: "Angular",
|
|
689
|
+
type: "STATIC",
|
|
690
|
+
match: (deps) => "@angular/core" in deps,
|
|
691
|
+
buildCommand: "npm run build",
|
|
692
|
+
startCommand: null,
|
|
693
|
+
outputDir: "dist",
|
|
694
|
+
port: 4200
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
name: "express",
|
|
698
|
+
label: "Express",
|
|
699
|
+
type: "WEB",
|
|
700
|
+
match: (deps) => "express" in deps,
|
|
701
|
+
buildCommand: null,
|
|
702
|
+
startCommand: "node index.js",
|
|
703
|
+
outputDir: null,
|
|
704
|
+
port: 3e3
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "fastify",
|
|
708
|
+
label: "Fastify",
|
|
709
|
+
type: "WEB",
|
|
710
|
+
match: (deps) => "fastify" in deps,
|
|
711
|
+
buildCommand: null,
|
|
712
|
+
startCommand: "node index.js",
|
|
713
|
+
outputDir: null,
|
|
714
|
+
port: 3e3
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: "nestjs",
|
|
718
|
+
label: "NestJS",
|
|
719
|
+
type: "WEB",
|
|
720
|
+
match: (deps) => "@nestjs/core" in deps,
|
|
721
|
+
buildCommand: "npm run build",
|
|
722
|
+
startCommand: "npm run start:prod",
|
|
723
|
+
outputDir: "dist",
|
|
724
|
+
port: 3e3
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: "hono",
|
|
728
|
+
label: "Hono",
|
|
729
|
+
type: "WEB",
|
|
730
|
+
match: (deps) => "hono" in deps || "@hono/node-server" in deps,
|
|
731
|
+
buildCommand: null,
|
|
732
|
+
startCommand: "node index.js",
|
|
733
|
+
outputDir: null,
|
|
734
|
+
port: 3e3
|
|
735
|
+
}
|
|
736
|
+
];
|
|
737
|
+
function detectPackageManager$1(files) {
|
|
738
|
+
if ("bun.lockb" in files || "bun.lock" in files) return "bun";
|
|
739
|
+
if ("pnpm-lock.yaml" in files || "pnpm-workspace.yaml" in files) return "pnpm";
|
|
740
|
+
if ("yarn.lock" in files) return "yarn";
|
|
741
|
+
return "npm";
|
|
742
|
+
}
|
|
743
|
+
function runCmd(pm, script) {
|
|
744
|
+
switch (pm) {
|
|
745
|
+
case "bun": return `bun run ${script}`;
|
|
746
|
+
case "yarn": return `yarn ${script}`;
|
|
747
|
+
case "pnpm": return `pnpm run ${script}`;
|
|
748
|
+
default: return `npm run ${script}`;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function startCmd(pm) {
|
|
752
|
+
switch (pm) {
|
|
753
|
+
case "bun": return "bun run start";
|
|
754
|
+
case "yarn": return "yarn start";
|
|
755
|
+
case "pnpm": return "pnpm start";
|
|
756
|
+
default: return "npm start";
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function safeParse(json) {
|
|
760
|
+
if (!json) return null;
|
|
761
|
+
try {
|
|
762
|
+
return JSON.parse(json);
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function getAllDeps(pkg) {
|
|
768
|
+
const deps = pkg.dependencies ?? {};
|
|
769
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
770
|
+
return {
|
|
771
|
+
...deps,
|
|
772
|
+
...devDeps
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function detectFramework(packageJsonStr, pm = "npm") {
|
|
776
|
+
const pkg = safeParse(packageJsonStr);
|
|
777
|
+
if (!pkg) return null;
|
|
778
|
+
const allDeps = getAllDeps(pkg);
|
|
779
|
+
const scripts = pkg.scripts ?? {};
|
|
780
|
+
for (const rule of FRAMEWORK_RULES) if (rule.match(allDeps)) {
|
|
781
|
+
const ruleStart = rule.startCommand === "node index.js" ? "node index.js" : rule.startCommand ? startCmd(pm) : null;
|
|
782
|
+
let buildScript = "build";
|
|
783
|
+
let startScript = "start";
|
|
784
|
+
const scriptKeys = Object.keys(scripts);
|
|
785
|
+
const buildVariants = scriptKeys.filter((k) => k.startsWith("build:") || k.startsWith("build-"));
|
|
786
|
+
const startVariants = scriptKeys.filter((k) => k.startsWith("start:") || k.startsWith("start-"));
|
|
787
|
+
if (buildVariants.length > 0 && !scripts.build) buildScript = buildVariants[0];
|
|
788
|
+
if (startVariants.length > 0 && !scripts.start) startScript = startVariants[0];
|
|
789
|
+
return {
|
|
790
|
+
name: rule.name,
|
|
791
|
+
label: rule.label,
|
|
792
|
+
type: rule.type,
|
|
793
|
+
buildCommand: scripts[buildScript] ? runCmd(pm, buildScript) : rule.buildCommand ? runCmd(pm, "build") : null,
|
|
794
|
+
startCommand: scripts[startScript] ? runCmd(pm, startScript) : ruleStart,
|
|
795
|
+
outputDir: rule.outputDir,
|
|
796
|
+
port: rule.port
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (scripts.build || scripts.start) {
|
|
800
|
+
let outputDir = null;
|
|
801
|
+
if (scripts.build) {
|
|
802
|
+
if (scripts.build.includes("tsc")) outputDir = "dist";
|
|
803
|
+
else if (scripts.build.includes("tsup")) outputDir = "dist";
|
|
804
|
+
else if (scripts.build.includes("esbuild")) outputDir = "dist";
|
|
805
|
+
else if (scripts.build.includes("webpack")) outputDir = "dist";
|
|
806
|
+
else if (scripts.build.includes("rollup")) outputDir = "dist";
|
|
807
|
+
else if (scripts.build.includes("parcel")) outputDir = "dist";
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
name: "node",
|
|
811
|
+
label: "Node.js",
|
|
812
|
+
type: scripts.start ? "WEB" : "STATIC",
|
|
813
|
+
buildCommand: scripts.build ? runCmd(pm, "build") : null,
|
|
814
|
+
startCommand: scripts.start ? startCmd(pm) : null,
|
|
815
|
+
outputDir,
|
|
816
|
+
port: 3e3
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function detectEnvVars(files) {
|
|
822
|
+
const envContent = files[".env.example"] ?? files[".env.sample"] ?? files[".env.local.example"] ?? files[".env"];
|
|
823
|
+
if (!envContent) return [];
|
|
824
|
+
const vars = [];
|
|
825
|
+
for (const line of envContent.split("\n")) {
|
|
826
|
+
const trimmed = line.trim();
|
|
827
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
828
|
+
const eqIdx = trimmed.indexOf("=");
|
|
829
|
+
if (eqIdx === -1) continue;
|
|
830
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
831
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
832
|
+
if (key) vars.push({
|
|
833
|
+
key,
|
|
834
|
+
value
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
return vars;
|
|
838
|
+
}
|
|
839
|
+
function resolveWorkspacePatterns(patterns, availableFiles) {
|
|
840
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
841
|
+
for (const pattern of patterns) {
|
|
842
|
+
const hasGlob = /\/?\*\*?$/.test(pattern);
|
|
843
|
+
const base = pattern.replace(/\/?\*\*?$/, "");
|
|
844
|
+
if (hasGlob) {
|
|
845
|
+
for (const filePath of availableFiles) if (filePath.startsWith(base + "/") && filePath.endsWith("/package.json")) {
|
|
846
|
+
const parts = filePath.slice(base.length + 1).split("/");
|
|
847
|
+
if (parts.length === 2 && parts[1] === "package.json") dirs.add(base + "/" + parts[0]);
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
const pkgPath = base + "/package.json";
|
|
851
|
+
if (availableFiles.includes(pkgPath)) dirs.add(base);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return [...dirs];
|
|
855
|
+
}
|
|
856
|
+
function detectMonorepo(files, pm = "npm") {
|
|
857
|
+
const rootPkg = safeParse(files["package.json"]);
|
|
858
|
+
const availableFiles = Object.keys(files);
|
|
859
|
+
let workspacePatterns = [];
|
|
860
|
+
if (rootPkg?.workspaces) {
|
|
861
|
+
const ws = rootPkg.workspaces;
|
|
862
|
+
if (Array.isArray(ws)) workspacePatterns = ws;
|
|
863
|
+
else if (typeof ws === "object" && ws !== null && "packages" in ws && Array.isArray(ws.packages)) workspacePatterns = ws.packages;
|
|
864
|
+
}
|
|
865
|
+
if (workspacePatterns.length === 0 && files["pnpm-workspace.yaml"]) {
|
|
866
|
+
const lines = files["pnpm-workspace.yaml"].split("\n");
|
|
867
|
+
let inPackages = false;
|
|
868
|
+
for (const line of lines) {
|
|
869
|
+
if (line.match(/^packages\s*:/)) {
|
|
870
|
+
inPackages = true;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (inPackages) {
|
|
874
|
+
const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
|
|
875
|
+
if (match) workspacePatterns.push(match[1]);
|
|
876
|
+
else if (line.match(/^\S/)) break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (workspacePatterns.length === 0) return {
|
|
881
|
+
isMonorepo: false,
|
|
882
|
+
apps: []
|
|
883
|
+
};
|
|
884
|
+
const dirs = resolveWorkspacePatterns(workspacePatterns, availableFiles);
|
|
885
|
+
const rootScripts = rootPkg?.scripts || {};
|
|
886
|
+
const apps = [];
|
|
887
|
+
for (const dir of dirs) {
|
|
888
|
+
const pkgContent = files[`${dir}/package.json`];
|
|
889
|
+
const pkg = safeParse(pkgContent);
|
|
890
|
+
const name = pkg?.name ?? dir.split("/").pop() ?? dir;
|
|
891
|
+
let framework = detectFramework(pkgContent, pm);
|
|
892
|
+
if (framework && rootScripts) {
|
|
893
|
+
const serviceName = dir.split("/").pop() ?? name;
|
|
894
|
+
const buildKey = [
|
|
895
|
+
`build:${serviceName}`,
|
|
896
|
+
`build-${serviceName}`,
|
|
897
|
+
`${serviceName}:build`
|
|
898
|
+
].find((k) => rootScripts[k]);
|
|
899
|
+
if (buildKey) framework.buildCommand = `${pm} run ${buildKey}`;
|
|
900
|
+
const startKey = [
|
|
901
|
+
`start:${serviceName}`,
|
|
902
|
+
`start-${serviceName}`,
|
|
903
|
+
`${serviceName}:start`,
|
|
904
|
+
`dev:${serviceName}`,
|
|
905
|
+
`serve:${serviceName}`
|
|
906
|
+
].find((k) => rootScripts[k]);
|
|
907
|
+
if (startKey) framework.startCommand = `${pm} run ${startKey}`;
|
|
908
|
+
if (framework.name === "nextjs" && !startKey) framework.startCommand = `cd ${dir} && ${pm} run start`;
|
|
909
|
+
else if ([
|
|
910
|
+
"hono",
|
|
911
|
+
"express",
|
|
912
|
+
"fastify",
|
|
913
|
+
"nestjs"
|
|
914
|
+
].includes(framework.name)) {
|
|
915
|
+
if (!startKey && framework.buildCommand) {
|
|
916
|
+
const scripts = pkg?.scripts || {};
|
|
917
|
+
if (scripts.build && scripts.build.includes("tsc")) framework.startCommand = `node ${dir}/dist/index.js`;
|
|
918
|
+
else if (scripts.build && scripts.build.includes("tsup")) framework.startCommand = `node ${dir}/dist/index.js`;
|
|
919
|
+
else if (scripts.build && scripts.build.includes("esbuild")) framework.startCommand = `node ${dir}/dist/index.js`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
apps.push({
|
|
924
|
+
name,
|
|
925
|
+
path: dir,
|
|
926
|
+
framework
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
isMonorepo: apps.length > 0,
|
|
931
|
+
apps
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function analyzeRepo(files) {
|
|
935
|
+
const pm = detectPackageManager$1(files);
|
|
936
|
+
const framework = detectFramework(files["package.json"], pm);
|
|
937
|
+
const envVars = detectEnvVars(files);
|
|
938
|
+
const { isMonorepo, apps } = detectMonorepo(files, pm);
|
|
939
|
+
return {
|
|
940
|
+
framework,
|
|
941
|
+
packageManager: pm,
|
|
942
|
+
envVars,
|
|
943
|
+
isMonorepo,
|
|
944
|
+
monorepoApps: apps
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/lib/upload.ts
|
|
950
|
+
const DEFAULT_IGNORE = [
|
|
951
|
+
".git",
|
|
952
|
+
"node_modules",
|
|
953
|
+
".env",
|
|
954
|
+
".env.local",
|
|
955
|
+
".env.*.local",
|
|
956
|
+
"dist",
|
|
957
|
+
"build",
|
|
958
|
+
".next",
|
|
959
|
+
".nuxt",
|
|
960
|
+
".output",
|
|
961
|
+
"*.log",
|
|
962
|
+
".DS_Store",
|
|
963
|
+
"Thumbs.db",
|
|
964
|
+
".idea",
|
|
965
|
+
".vscode",
|
|
966
|
+
".claude",
|
|
967
|
+
".cursor",
|
|
968
|
+
".sisyphus",
|
|
969
|
+
".playwright-mcp",
|
|
970
|
+
"*.swp",
|
|
971
|
+
"*.swo"
|
|
972
|
+
];
|
|
973
|
+
async function getIgnorePatterns(directory) {
|
|
974
|
+
const patterns = [...DEFAULT_IGNORE];
|
|
975
|
+
try {
|
|
976
|
+
const gitignoreContent = readFileSync(join(directory, ".gitignore"), "utf-8");
|
|
977
|
+
patterns.push(...gitignoreContent.split("\n").filter((line) => line.trim() && !line.startsWith("#")));
|
|
978
|
+
} catch {}
|
|
979
|
+
try {
|
|
980
|
+
const velozignoreContent = readFileSync(join(directory, ".velozignore"), "utf-8");
|
|
981
|
+
patterns.push(...velozignoreContent.split("\n").filter((line) => line.trim() && !line.startsWith("#")));
|
|
982
|
+
} catch {}
|
|
983
|
+
return patterns;
|
|
984
|
+
}
|
|
985
|
+
async function getFilesToUpload(directory) {
|
|
986
|
+
const ignorePatterns = await getIgnorePatterns(directory);
|
|
987
|
+
const ig = ignore().add(ignorePatterns);
|
|
988
|
+
const files = [];
|
|
989
|
+
async function walk(dir) {
|
|
990
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
991
|
+
for (const entry of entries) {
|
|
992
|
+
const fullPath = join(dir, entry.name);
|
|
993
|
+
const relativePath = relative(directory, fullPath);
|
|
994
|
+
if (ig.ignores(relativePath)) continue;
|
|
995
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
996
|
+
if (entry.isDirectory()) await walk(fullPath);
|
|
997
|
+
else if (entry.isFile()) files.push(fullPath);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
await walk(directory);
|
|
1001
|
+
return files;
|
|
1002
|
+
}
|
|
1003
|
+
async function createTarball(directory, extraFiles) {
|
|
1004
|
+
const createdFiles = [];
|
|
1005
|
+
if (extraFiles) for (const file of extraFiles) {
|
|
1006
|
+
const filePath = join(directory, file.name);
|
|
1007
|
+
if (!existsSync(filePath)) {
|
|
1008
|
+
writeFileSync(filePath, file.content);
|
|
1009
|
+
createdFiles.push(filePath);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const files = await getFilesToUpload(directory);
|
|
1014
|
+
if (files.length === 0) throw new Error("No files to upload");
|
|
1015
|
+
const tarPath = join(await mkdtemp(join(tmpdir(), "veloz-upload-")), "source.tar.gz");
|
|
1016
|
+
const relativePaths = files.map((f) => relative(directory, f));
|
|
1017
|
+
for (const f of createdFiles) {
|
|
1018
|
+
const rel = relative(directory, f);
|
|
1019
|
+
if (!relativePaths.includes(rel)) relativePaths.push(rel);
|
|
1020
|
+
}
|
|
1021
|
+
await tar.create({
|
|
1022
|
+
gzip: true,
|
|
1023
|
+
file: tarPath,
|
|
1024
|
+
cwd: directory
|
|
1025
|
+
}, relativePaths);
|
|
1026
|
+
return {
|
|
1027
|
+
path: tarPath,
|
|
1028
|
+
size: statSync(tarPath).size
|
|
1029
|
+
};
|
|
1030
|
+
} finally {
|
|
1031
|
+
for (const f of createdFiles) try {
|
|
1032
|
+
unlinkSync(f);
|
|
1033
|
+
} catch {}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
async function uploadSource(apiUrl, deploymentId, directory, token, extraFiles) {
|
|
1037
|
+
const { path: tarPath } = await createTarball(directory, extraFiles);
|
|
1038
|
+
try {
|
|
1039
|
+
const urlResponse = await fetch(`${apiUrl}/api/deployments/${deploymentId}/upload-url`, {
|
|
1040
|
+
method: "POST",
|
|
1041
|
+
headers: {
|
|
1042
|
+
Authorization: `Bearer ${token}`,
|
|
1043
|
+
"Content-Type": "application/json"
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
if (!urlResponse.ok) {
|
|
1047
|
+
const err = await urlResponse.json().catch(() => ({ error: "Unknown error" }));
|
|
1048
|
+
throw new Error(`Failed to get upload URL: ${err.error || urlResponse.statusText}`);
|
|
1049
|
+
}
|
|
1050
|
+
const { uploadUrl, objectKey } = await urlResponse.json();
|
|
1051
|
+
const fileBuffer = readFileSync(tarPath);
|
|
1052
|
+
const putResponse = await fetch(uploadUrl, {
|
|
1053
|
+
method: "PUT",
|
|
1054
|
+
headers: { "Content-Type": "application/gzip" },
|
|
1055
|
+
body: fileBuffer
|
|
1056
|
+
});
|
|
1057
|
+
if (!putResponse.ok) throw new Error(`S3 upload failed: ${putResponse.status} ${putResponse.statusText}`);
|
|
1058
|
+
const buildResponse = await fetch(`${apiUrl}/api/deployments/${deploymentId}/build`, {
|
|
1059
|
+
method: "POST",
|
|
1060
|
+
headers: {
|
|
1061
|
+
Authorization: `Bearer ${token}`,
|
|
1062
|
+
"Content-Type": "application/json"
|
|
1063
|
+
},
|
|
1064
|
+
body: JSON.stringify({ objectKey })
|
|
1065
|
+
});
|
|
1066
|
+
if (!buildResponse.ok) {
|
|
1067
|
+
const err = await buildResponse.json().catch(() => ({ error: "Unknown error" }));
|
|
1068
|
+
throw new Error(`Failed to trigger build: ${err.error || buildResponse.statusText}`);
|
|
1069
|
+
}
|
|
1070
|
+
} finally {
|
|
1071
|
+
await rm(tarPath, {
|
|
1072
|
+
force: true,
|
|
1073
|
+
recursive: true
|
|
1074
|
+
}).catch(() => {});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
async function calculateDirectorySize(directory) {
|
|
1078
|
+
const files = await getFilesToUpload(directory);
|
|
1079
|
+
let totalSize = 0;
|
|
1080
|
+
for (const file of files) {
|
|
1081
|
+
const stats = await stat(file);
|
|
1082
|
+
totalSize += stats.size;
|
|
1083
|
+
}
|
|
1084
|
+
return totalSize;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/lib/deploy-stream.ts
|
|
1089
|
+
const statusLabels$1 = {
|
|
1090
|
+
QUEUED: "Na fila",
|
|
1091
|
+
BUILDING: "Construindo",
|
|
1092
|
+
BUILD_FAILED: "Falha na construção",
|
|
1093
|
+
DEPLOYING: "Implantando",
|
|
1094
|
+
LIVE: "Ativo",
|
|
1095
|
+
FAILED: "Falhou",
|
|
1096
|
+
CANCELLED: "Cancelado"
|
|
1097
|
+
};
|
|
1098
|
+
const TERMINAL_STATUSES$1 = new Set([
|
|
1099
|
+
"LIVE",
|
|
1100
|
+
"BUILD_FAILED",
|
|
1101
|
+
"FAILED",
|
|
1102
|
+
"CANCELLED"
|
|
1103
|
+
]);
|
|
1104
|
+
async function streamDeploymentLogs(client, deploymentId, _serviceId, serviceName) {
|
|
1105
|
+
const isVerbose = process.env.VELOZ_VERBOSE === "true";
|
|
1106
|
+
const logHeader = serviceName ? `📦 Build logs para ${chalk.bold(serviceName)}:` : "📦 Build logs:";
|
|
1107
|
+
console.log(chalk.cyan(`\n${logHeader}`));
|
|
1108
|
+
console.log(chalk.dim("─".repeat(60)));
|
|
1109
|
+
if (isVerbose) console.log(chalk.dim(`[verbose] Conectando ao stream de logs para deployment ${deploymentId}...`));
|
|
1110
|
+
let finalStatus = "";
|
|
1111
|
+
let logsReceived = false;
|
|
1112
|
+
try {
|
|
1113
|
+
const stream = await client.logs.streamBuildLogs({ deploymentId });
|
|
1114
|
+
for await (const event of stream) {
|
|
1115
|
+
logsReceived = true;
|
|
1116
|
+
if (event.type === "status") {
|
|
1117
|
+
const label = statusLabels$1[event.content] ?? event.content;
|
|
1118
|
+
const icon = event.content === "LIVE" ? chalk.green("●") : event.content === "BUILD_FAILED" || event.content === "FAILED" ? chalk.red("●") : chalk.yellow("●");
|
|
1119
|
+
process.stdout.write(`\n${icon} ${chalk.bold(label)}\n`);
|
|
1120
|
+
finalStatus = event.content;
|
|
1121
|
+
if (isVerbose) console.log(chalk.dim(`[verbose] Status mudou para: ${event.content}`));
|
|
1122
|
+
} else if (event.type === "log") process.stdout.write(event.content);
|
|
1123
|
+
}
|
|
1124
|
+
if (!logsReceived && isVerbose) console.log(chalk.yellow("[verbose] Nenhum log recebido do stream. Buscando status do deployment..."));
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (isVerbose) console.log(chalk.red(`[verbose] Erro no stream: ${error.message}`));
|
|
1127
|
+
try {
|
|
1128
|
+
const d = await client.deployments.get({ deploymentId });
|
|
1129
|
+
finalStatus = d.status;
|
|
1130
|
+
if (isVerbose) console.log(chalk.dim(`[verbose] Status do deployment: ${d.status}`));
|
|
1131
|
+
try {
|
|
1132
|
+
const logs = await client.logs.getBuildLogs({ deploymentId });
|
|
1133
|
+
if (logs.buildLogs) {
|
|
1134
|
+
console.log(chalk.dim("\n── Logs salvos ──\n"));
|
|
1135
|
+
process.stdout.write(logs.buildLogs);
|
|
1136
|
+
} else if (isVerbose) console.log(chalk.yellow("[verbose] Nenhum log salvo encontrado para este deployment"));
|
|
1137
|
+
} catch {
|
|
1138
|
+
if (isVerbose) console.log(chalk.yellow("[verbose] Não foi possível buscar logs de build"));
|
|
1139
|
+
}
|
|
1140
|
+
} catch (fetchError) {
|
|
1141
|
+
if (isVerbose) console.log(chalk.red(`[verbose] Erro ao buscar deployment: ${fetchError.message}`));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
console.log(chalk.dim("\n" + "─".repeat(60)));
|
|
1145
|
+
if (finalStatus === "LIVE") success(serviceName ? `Deploy de ${chalk.bold(serviceName)} concluído! Serviço está ativo.` : "Deploy concluído! Serviço está ativo.");
|
|
1146
|
+
else if (TERMINAL_STATUSES$1.has(finalStatus)) {
|
|
1147
|
+
const errorMsg = serviceName ? `✗ Deploy de ${chalk.bold(serviceName)} finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}` : `✗ Deploy finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}`;
|
|
1148
|
+
console.error(chalk.red(errorMsg));
|
|
1149
|
+
if (!serviceName) process.exit(1);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
//#endregion
|
|
1154
|
+
//#region src/lib/deploy-parallel.ts
|
|
1155
|
+
async function withRetry$1(fn, maxRetries = 3) {
|
|
1156
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
1157
|
+
return await fn();
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
if (attempt < maxRetries) {
|
|
1160
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
1161
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, delay));
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
throw new Error("Max retries exceeded");
|
|
1167
|
+
}
|
|
1168
|
+
const statusLabels = {
|
|
1169
|
+
QUEUED: "Na fila",
|
|
1170
|
+
BUILDING: "Construindo",
|
|
1171
|
+
BUILD_FAILED: "Falha na construção",
|
|
1172
|
+
DEPLOYING: "Implantando",
|
|
1173
|
+
LIVE: "Ativo",
|
|
1174
|
+
FAILED: "Falhou",
|
|
1175
|
+
CANCELLED: "Cancelado"
|
|
1176
|
+
};
|
|
1177
|
+
const statusIcons = {
|
|
1178
|
+
QUEUED: chalk.gray("○"),
|
|
1179
|
+
BUILDING: chalk.yellow("●"),
|
|
1180
|
+
DEPLOYING: chalk.blue("●"),
|
|
1181
|
+
LIVE: chalk.green("●"),
|
|
1182
|
+
BUILD_FAILED: chalk.red("●"),
|
|
1183
|
+
FAILED: chalk.red("●"),
|
|
1184
|
+
CANCELLED: chalk.gray("●")
|
|
1185
|
+
};
|
|
1186
|
+
const TERMINAL_STATUSES = new Set([
|
|
1187
|
+
"LIVE",
|
|
1188
|
+
"BUILD_FAILED",
|
|
1189
|
+
"FAILED",
|
|
1190
|
+
"CANCELLED"
|
|
1191
|
+
]);
|
|
1192
|
+
function renderProgress(progressMap, prevLineCount) {
|
|
1193
|
+
for (let i = 0; i < prevLineCount; i++) process.stdout.write("\x1B[1A\x1B[2K");
|
|
1194
|
+
let lineCount = 0;
|
|
1195
|
+
for (const [, progress] of progressMap) {
|
|
1196
|
+
const icon = statusIcons[progress.status] || chalk.gray("○");
|
|
1197
|
+
const label = statusLabels[progress.status] || progress.status;
|
|
1198
|
+
process.stdout.write(`${icon} ${chalk.bold(progress.serviceName)}: ${label}\n`);
|
|
1199
|
+
lineCount++;
|
|
1200
|
+
if (progress.status === "BUILDING" || progress.status === "BUILD_FAILED") {
|
|
1201
|
+
if (progress.logLines.length > 0) {
|
|
1202
|
+
const tail = progress.logLines.slice(-3);
|
|
1203
|
+
for (const line of tail) {
|
|
1204
|
+
const truncated = line.substring(0, 80) + (line.length > 80 ? "..." : "");
|
|
1205
|
+
process.stdout.write(` ${chalk.dim(truncated)}\n`);
|
|
1206
|
+
lineCount++;
|
|
1207
|
+
}
|
|
1208
|
+
} else if (progress.status === "BUILDING") {
|
|
1209
|
+
process.stdout.write(` ${chalk.dim("⏳ Aguardando logs do build...")}\n`);
|
|
1210
|
+
lineCount++;
|
|
1211
|
+
}
|
|
1212
|
+
} else if (progress.status === "QUEUED") {
|
|
1213
|
+
process.stdout.write(` ${chalk.dim("📋 Na fila para construção...")}\n`);
|
|
1214
|
+
lineCount++;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return lineCount;
|
|
1218
|
+
}
|
|
1219
|
+
async function deployServicesInParallel(client, services) {
|
|
1220
|
+
console.log(chalk.cyan("\n🚀 Iniciando deploy paralelo de múltiplos serviços...\n"));
|
|
1221
|
+
const progressMap = /* @__PURE__ */ new Map();
|
|
1222
|
+
const projectRoot = process.cwd();
|
|
1223
|
+
const sizeInBytes = await calculateDirectorySize(projectRoot);
|
|
1224
|
+
const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
|
|
1225
|
+
const deploymentPromises = services.map(async (service) => {
|
|
1226
|
+
try {
|
|
1227
|
+
const deployment = await withRetry$1(() => client.deployments.create({ serviceId: service.serviceId }));
|
|
1228
|
+
const authConfig = await requireAuth();
|
|
1229
|
+
await withRetry$1(() => uploadSource(authConfig.apiUrl, deployment.id, projectRoot, authConfig.apiKey, service.extraFiles));
|
|
1230
|
+
progressMap.set(service.serviceId, {
|
|
1231
|
+
serviceName: service.serviceName,
|
|
1232
|
+
deploymentId: deployment.id,
|
|
1233
|
+
status: "QUEUED",
|
|
1234
|
+
logLines: [],
|
|
1235
|
+
completed: false,
|
|
1236
|
+
success: false
|
|
1237
|
+
});
|
|
1238
|
+
console.log(`${chalk.green("✓")} ${chalk.bold(service.serviceName)}: Upload concluído (${sizeMB} MB)`);
|
|
1239
|
+
return {
|
|
1240
|
+
service,
|
|
1241
|
+
deploymentId: deployment.id
|
|
1242
|
+
};
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
console.log(`${chalk.red("✗")} ${chalk.bold(service.serviceName)}: Falha ao iniciar deploy`);
|
|
1245
|
+
progressMap.set(service.serviceId, {
|
|
1246
|
+
serviceName: service.serviceName,
|
|
1247
|
+
deploymentId: "",
|
|
1248
|
+
status: "FAILED",
|
|
1249
|
+
logLines: [],
|
|
1250
|
+
completed: true,
|
|
1251
|
+
success: false
|
|
1252
|
+
});
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
const activeDeployments = (await Promise.allSettled(deploymentPromises)).filter((d) => d.status === "fulfilled").map((d) => d.value);
|
|
1257
|
+
if (activeDeployments.length === 0) {
|
|
1258
|
+
console.error(chalk.red("\n✗ Todos os deploys falharam ao iniciar."));
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
console.log(chalk.cyan("\n📦 Monitorando progresso dos deploys:\n"));
|
|
1262
|
+
console.log(chalk.dim("─".repeat(60)) + "\n");
|
|
1263
|
+
let lineCount = 0;
|
|
1264
|
+
lineCount = renderProgress(progressMap, lineCount);
|
|
1265
|
+
const isVerbose = process.env.VELOZ_VERBOSE === "true";
|
|
1266
|
+
const streamPromises = activeDeployments.map(async ({ service, deploymentId }) => {
|
|
1267
|
+
try {
|
|
1268
|
+
await new Promise((resolve$1) => setTimeout(resolve$1, 1e3));
|
|
1269
|
+
if (isVerbose) console.log(chalk.dim(`\n[verbose] Conectando ao stream para ${service.serviceName} (${deploymentId})...`));
|
|
1270
|
+
const stream = await client.logs.streamBuildLogs({ deploymentId });
|
|
1271
|
+
for await (const event of stream) {
|
|
1272
|
+
const progress = progressMap.get(service.serviceId);
|
|
1273
|
+
if (!progress) continue;
|
|
1274
|
+
if (event.type === "status") {
|
|
1275
|
+
progress.status = event.content;
|
|
1276
|
+
if (TERMINAL_STATUSES.has(event.content)) {
|
|
1277
|
+
progress.completed = true;
|
|
1278
|
+
progress.success = event.content === "LIVE";
|
|
1279
|
+
}
|
|
1280
|
+
if (isVerbose) console.log(chalk.dim(`\n[verbose] ${service.serviceName}: status mudou para ${event.content}`));
|
|
1281
|
+
} else if (event.type === "log") {
|
|
1282
|
+
const newLines = event.content.split("\n").filter((l) => l.trim());
|
|
1283
|
+
progress.logLines.push(...newLines);
|
|
1284
|
+
}
|
|
1285
|
+
lineCount = renderProgress(progressMap, lineCount);
|
|
1286
|
+
}
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
const progress = progressMap.get(service.serviceId);
|
|
1289
|
+
if (progress && !progress.completed) {
|
|
1290
|
+
if (isVerbose) console.error(`\n${chalk.red("[verbose]")} Error streaming logs for ${service.serviceName}:`, error.message);
|
|
1291
|
+
else console.error(`\n${chalk.red("✗")} Error streaming logs for ${service.serviceName}`);
|
|
1292
|
+
progress.status = "FAILED";
|
|
1293
|
+
progress.completed = true;
|
|
1294
|
+
lineCount = renderProgress(progressMap, lineCount);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
await Promise.all(streamPromises);
|
|
1299
|
+
renderProgress(progressMap, lineCount);
|
|
1300
|
+
console.log(chalk.dim("\n" + "─".repeat(60)));
|
|
1301
|
+
const successful = Array.from(progressMap.values()).filter((p) => p.success);
|
|
1302
|
+
const failed = Array.from(progressMap.values()).filter((p) => !p.success);
|
|
1303
|
+
if (successful.length > 0) {
|
|
1304
|
+
console.log(chalk.green(`\n✅ ${successful.length} serviço(s) implantado(s) com sucesso:\n`));
|
|
1305
|
+
for (const progress of successful) console.log(` ${chalk.green("✓")} ${chalk.bold(progress.serviceName)}`);
|
|
1306
|
+
}
|
|
1307
|
+
if (failed.length > 0) {
|
|
1308
|
+
console.log(chalk.red(`\n✗ ${failed.length} serviço(s) falhou(aram):\n`));
|
|
1309
|
+
for (const progress of failed) {
|
|
1310
|
+
console.log(` ${chalk.red("✗")} ${chalk.bold(progress.serviceName)}`);
|
|
1311
|
+
if (progress.logLines.length > 0) {
|
|
1312
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
1313
|
+
const tail = progress.logLines.slice(-50);
|
|
1314
|
+
for (const line of tail) console.log(` ${chalk.dim(line)}`);
|
|
1315
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (successful.length > 0) info("\nUse 'veloz logs -f' para acompanhar os logs de execução.");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
//#endregion
|
|
1323
|
+
//#region src/lib/dockerfile-generator.ts
|
|
1324
|
+
function pmSetupInstructions(pm) {
|
|
1325
|
+
switch (pm) {
|
|
1326
|
+
case "pnpm": return "RUN corepack enable && corepack prepare pnpm@latest --activate";
|
|
1327
|
+
case "yarn": return "RUN corepack enable";
|
|
1328
|
+
case "bun": return "RUN apk add --no-cache bash curl && curl -fsSL https://bun.sh/install | bash && ln -s /root/.bun/bin/bun /usr/local/bin/bun";
|
|
1329
|
+
case "npm": return "";
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function installCommand(pm) {
|
|
1333
|
+
switch (pm) {
|
|
1334
|
+
case "bun": return "bun install --frozen-lockfile";
|
|
1335
|
+
case "pnpm": return "pnpm install --frozen-lockfile";
|
|
1336
|
+
case "yarn": return "yarn install --frozen-lockfile";
|
|
1337
|
+
case "npm": return "npm ci";
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function lockfileNames(pm) {
|
|
1341
|
+
switch (pm) {
|
|
1342
|
+
case "bun": return ["bun.lockb", "bun.lock"];
|
|
1343
|
+
case "pnpm": return ["pnpm-lock.yaml"];
|
|
1344
|
+
case "yarn": return ["yarn.lock"];
|
|
1345
|
+
case "npm": return ["package-lock.json"];
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function generateWebDockerfile(opts) {
|
|
1349
|
+
const { nodeVersion, pm, buildCommand, startCommand, rootDirectory, port = 3e3 } = opts;
|
|
1350
|
+
const setup = pmSetupInstructions(pm);
|
|
1351
|
+
const lockfiles = lockfileNames(pm);
|
|
1352
|
+
const hasRoot = !!(rootDirectory && rootDirectory !== "/");
|
|
1353
|
+
const cleanRoot = hasRoot ? rootDirectory.replace(/^\//, "") : "";
|
|
1354
|
+
const depsCopy = hasRoot ? [
|
|
1355
|
+
`# Root-level dependency files`,
|
|
1356
|
+
`COPY package.json ${lockfiles.join(" ")} ./`,
|
|
1357
|
+
`# Service-level dependency files`,
|
|
1358
|
+
`COPY ${cleanRoot}/package.json ./${cleanRoot}/`
|
|
1359
|
+
] : [`COPY package.json ${lockfiles.join(" ")} ./`];
|
|
1360
|
+
const workdirPrefix = hasRoot ? `cd ${cleanRoot} && ` : "";
|
|
1361
|
+
const defaultStart = pm === "bun" ? "bun start" : `${pm} run start`;
|
|
1362
|
+
const finalStart = startCommand || defaultStart;
|
|
1363
|
+
return `# ── Stage 1: Install & Build ────────────────────────────
|
|
1364
|
+
FROM node:${nodeVersion}-alpine AS builder
|
|
1365
|
+
|
|
1366
|
+
WORKDIR /app
|
|
1367
|
+
${setup ? "\n" + setup + "\n" : ""}
|
|
1368
|
+
# Install dependencies (cached layer)
|
|
1369
|
+
${depsCopy.join("\n")}
|
|
1370
|
+
RUN ${installCommand(pm)}
|
|
1371
|
+
|
|
1372
|
+
# Copy full source
|
|
1373
|
+
COPY . .
|
|
1374
|
+
|
|
1375
|
+
# Build with env vars from BuildKit secret mount
|
|
1376
|
+
RUN --mount=type=secret,id=build-env \\
|
|
1377
|
+
set -a && \\
|
|
1378
|
+
if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
|
|
1379
|
+
set +a && \\
|
|
1380
|
+
${workdirPrefix}${buildCommand}
|
|
1381
|
+
|
|
1382
|
+
# ── Stage 2: Production runner ─────────────────────────
|
|
1383
|
+
FROM node:${nodeVersion}-alpine
|
|
1384
|
+
|
|
1385
|
+
WORKDIR /app
|
|
1386
|
+
|
|
1387
|
+
# Copy built app (node_modules + build output)
|
|
1388
|
+
COPY --from=builder /app .
|
|
1389
|
+
|
|
1390
|
+
ENV NODE_ENV=production
|
|
1391
|
+
ENV PORT=${port}
|
|
1392
|
+
EXPOSE ${port}
|
|
1393
|
+
|
|
1394
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\
|
|
1395
|
+
CMD wget --quiet --tries=1 --spider http://localhost:${port}/ || exit 1
|
|
1396
|
+
|
|
1397
|
+
CMD ${workdirPrefix ? `["sh", "-c", "${workdirPrefix}${finalStart}"]` : JSON.stringify(finalStart.split(" "))}
|
|
1398
|
+
`;
|
|
1399
|
+
}
|
|
1400
|
+
function generateStaticDockerfile(opts) {
|
|
1401
|
+
const { nodeVersion, pm, buildCommand, outputDir, rootDirectory } = opts;
|
|
1402
|
+
const setup = pmSetupInstructions(pm);
|
|
1403
|
+
const lockfiles = lockfileNames(pm);
|
|
1404
|
+
const hasRoot = !!(rootDirectory && rootDirectory !== "/");
|
|
1405
|
+
const cleanRoot = hasRoot ? rootDirectory.replace(/^\//, "") : "";
|
|
1406
|
+
const servicePrefix = hasRoot ? cleanRoot + "/" : "";
|
|
1407
|
+
const depsCopy = hasRoot ? [`COPY package.json ${lockfiles.join(" ")} ./`, `COPY ${cleanRoot}/package.json ./${cleanRoot}/`] : [`COPY package.json ${lockfiles.join(" ")} ./`];
|
|
1408
|
+
const workdirPrefix = hasRoot ? `cd ${cleanRoot} && ` : "";
|
|
1409
|
+
const outputDetection = outputDir ? `ENV OUTPUT_DIR="${outputDir}"` : [
|
|
1410
|
+
`# Auto-detect output directory`,
|
|
1411
|
+
`RUN if [ -d "${servicePrefix}dist" ]; then echo "${servicePrefix}dist" > /tmp/output-dir; \\`,
|
|
1412
|
+
` elif [ -d "${servicePrefix}build" ]; then echo "${servicePrefix}build" > /tmp/output-dir; \\`,
|
|
1413
|
+
` elif [ -d "${servicePrefix}out" ]; then echo "${servicePrefix}out" > /tmp/output-dir; \\`,
|
|
1414
|
+
` elif [ -d "${servicePrefix}.next/out" ]; then echo "${servicePrefix}.next/out" > /tmp/output-dir; \\`,
|
|
1415
|
+
` elif [ -d "${servicePrefix}public" ]; then echo "${servicePrefix}public" > /tmp/output-dir; \\`,
|
|
1416
|
+
` else echo "${servicePrefix}dist" > /tmp/output-dir; fi`
|
|
1417
|
+
].join("\n");
|
|
1418
|
+
const outputDirRef = outputDir ? servicePrefix + outputDir : "$(cat /tmp/output-dir)";
|
|
1419
|
+
return `# ── Stage 1: Build ──────────────────────────────────────
|
|
1420
|
+
FROM node:${nodeVersion}-alpine AS builder
|
|
1421
|
+
|
|
1422
|
+
WORKDIR /app
|
|
1423
|
+
${setup ? "\n" + setup + "\n" : ""}
|
|
1424
|
+
# Install dependencies (cached layer)
|
|
1425
|
+
${depsCopy.join("\n")}
|
|
1426
|
+
RUN ${installCommand(pm)}
|
|
1427
|
+
|
|
1428
|
+
# Copy full source
|
|
1429
|
+
COPY . .
|
|
1430
|
+
|
|
1431
|
+
# Build with env vars from BuildKit secret mount
|
|
1432
|
+
RUN --mount=type=secret,id=build-env \\
|
|
1433
|
+
set -a && \\
|
|
1434
|
+
if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
|
|
1435
|
+
set +a && \\
|
|
1436
|
+
${workdirPrefix}${buildCommand}
|
|
1437
|
+
|
|
1438
|
+
# Detect output directory
|
|
1439
|
+
${outputDetection}
|
|
1440
|
+
|
|
1441
|
+
# Process _headers and _redirects into nginx config
|
|
1442
|
+
COPY nginx.conf /tmp/nginx-base.conf
|
|
1443
|
+
COPY generate-nginx-config.cjs /tmp/generate-nginx-config.cjs
|
|
1444
|
+
RUN node /tmp/generate-nginx-config.cjs \\
|
|
1445
|
+
--base /tmp/nginx-base.conf \\
|
|
1446
|
+
--output-dir /app/${outputDirRef} \\
|
|
1447
|
+
--out /tmp/nginx-final.conf
|
|
1448
|
+
|
|
1449
|
+
# Copy build output to a known location
|
|
1450
|
+
RUN cp -r /app/${outputDirRef} /output
|
|
1451
|
+
|
|
1452
|
+
# ── Stage 2: Serve ─────────────────────────────────────
|
|
1453
|
+
FROM nginx:alpine
|
|
1454
|
+
|
|
1455
|
+
# Remove default nginx site
|
|
1456
|
+
RUN rm -f /etc/nginx/conf.d/default.conf
|
|
1457
|
+
|
|
1458
|
+
# Copy processed nginx config and static files
|
|
1459
|
+
COPY --from=builder /tmp/nginx-final.conf /etc/nginx/conf.d/default.conf
|
|
1460
|
+
COPY --from=builder /output /usr/share/nginx/html
|
|
1461
|
+
|
|
1462
|
+
EXPOSE 80
|
|
1463
|
+
|
|
1464
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
1465
|
+
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
|
|
1466
|
+
|
|
1467
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
1468
|
+
`;
|
|
1469
|
+
}
|
|
1470
|
+
function generateDockerfile(opts) {
|
|
1471
|
+
if (opts.serviceType === "STATIC") return generateStaticDockerfile(opts);
|
|
1472
|
+
return generateWebDockerfile(opts);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/lib/templates.ts
|
|
1477
|
+
/**
|
|
1478
|
+
* Embedded templates for STATIC site builds.
|
|
1479
|
+
* These are written to the project directory temporarily before creating the tar,
|
|
1480
|
+
* then cleaned up immediately after.
|
|
1481
|
+
*/
|
|
1482
|
+
const NGINX_CONF = `server {
|
|
1483
|
+
listen 80;
|
|
1484
|
+
server_name _;
|
|
1485
|
+
root /usr/share/nginx/html;
|
|
1486
|
+
index index.html;
|
|
1487
|
+
|
|
1488
|
+
# SPA routing - fallback to index.html for client-side routing
|
|
1489
|
+
location / {
|
|
1490
|
+
try_files \\$uri \\$uri/ /index.html;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
# Gzip compression
|
|
1494
|
+
gzip on;
|
|
1495
|
+
gzip_vary on;
|
|
1496
|
+
gzip_min_length 256;
|
|
1497
|
+
gzip_types
|
|
1498
|
+
text/plain
|
|
1499
|
+
text/css
|
|
1500
|
+
text/xml
|
|
1501
|
+
text/javascript
|
|
1502
|
+
application/json
|
|
1503
|
+
application/javascript
|
|
1504
|
+
application/xml+rss
|
|
1505
|
+
application/rss+xml
|
|
1506
|
+
application/atom+xml
|
|
1507
|
+
image/svg+xml
|
|
1508
|
+
text/x-component
|
|
1509
|
+
text/x-cross-domain-policy;
|
|
1510
|
+
|
|
1511
|
+
# Cache hashed assets (fingerprinted files)
|
|
1512
|
+
location ~* \\\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|webp|avif)\\$ {
|
|
1513
|
+
expires 1y;
|
|
1514
|
+
add_header Cache-Control "public, immutable";
|
|
1515
|
+
access_log off;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
# No cache for HTML files
|
|
1519
|
+
location ~* \\\\.html\\$ {
|
|
1520
|
+
expires -1;
|
|
1521
|
+
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
1522
|
+
add_header Pragma "no-cache";
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
# Security headers
|
|
1526
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
1527
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
1528
|
+
add_header X-XSS-Protection "1; mode=block" always;
|
|
1529
|
+
|
|
1530
|
+
# Custom headers placeholder (will be replaced during build)
|
|
1531
|
+
# CUSTOM_HEADERS_PLACEHOLDER
|
|
1532
|
+
|
|
1533
|
+
# Custom redirects placeholder (will be replaced during build)
|
|
1534
|
+
# CUSTOM_REDIRECTS_PLACEHOLDER
|
|
1535
|
+
}`;
|
|
1536
|
+
const GENERATE_NGINX_CONFIG_CJS = `#!/usr/bin/env node
|
|
1537
|
+
"use strict";
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Generates a final nginx.conf by processing Netlify-style _headers and _redirects
|
|
1541
|
+
* files from the static site build output. Runs inside the Docker build stage.
|
|
1542
|
+
*
|
|
1543
|
+
* Usage: node generate-nginx-config.cjs --base <nginx.conf> --output-dir <dir> --out <dest>
|
|
1544
|
+
*/
|
|
1545
|
+
|
|
1546
|
+
const fs = require("fs");
|
|
1547
|
+
const path = require("path");
|
|
1548
|
+
|
|
1549
|
+
// ── Parse CLI args ─────────────────────────────────────
|
|
1550
|
+
|
|
1551
|
+
const args = process.argv.slice(2);
|
|
1552
|
+
let baseConfig = "";
|
|
1553
|
+
let outputDir = "";
|
|
1554
|
+
let outFile = "";
|
|
1555
|
+
|
|
1556
|
+
for (let i = 0; i < args.length; i++) {
|
|
1557
|
+
if (args[i] === "--base" && args[i + 1]) baseConfig = args[++i];
|
|
1558
|
+
if (args[i] === "--output-dir" && args[i + 1]) outputDir = args[++i];
|
|
1559
|
+
if (args[i] === "--out" && args[i + 1]) outFile = args[++i];
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (!baseConfig || !outputDir || !outFile) {
|
|
1563
|
+
console.error(
|
|
1564
|
+
"Usage: node generate-nginx-config.cjs --base <nginx.conf> --output-dir <dir> --out <dest>"
|
|
1565
|
+
);
|
|
1566
|
+
process.exit(1);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
let nginxConfig = fs.readFileSync(baseConfig, "utf-8");
|
|
1570
|
+
|
|
1571
|
+
// ── Parse _headers (Netlify-style) ─────────────────────
|
|
1572
|
+
//
|
|
1573
|
+
// Format:
|
|
1574
|
+
// /api/*
|
|
1575
|
+
// Access-Control-Allow-Origin: *
|
|
1576
|
+
// /*
|
|
1577
|
+
// X-Frame-Options: DENY
|
|
1578
|
+
|
|
1579
|
+
const headersPath = path.join(outputDir, "_headers");
|
|
1580
|
+
if (fs.existsSync(headersPath)) {
|
|
1581
|
+
const content = fs.readFileSync(headersPath, "utf-8");
|
|
1582
|
+
let headerDirectives = "";
|
|
1583
|
+
let currentPath = "";
|
|
1584
|
+
let inLocationBlock = false;
|
|
1585
|
+
|
|
1586
|
+
for (const line of content.split("\\n")) {
|
|
1587
|
+
const trimmed = line.trim();
|
|
1588
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1589
|
+
|
|
1590
|
+
// Path pattern (starts without whitespace)
|
|
1591
|
+
if (!line.startsWith(" ") && !line.startsWith("\\t")) {
|
|
1592
|
+
// Close previous location block if open
|
|
1593
|
+
if (inLocationBlock) {
|
|
1594
|
+
headerDirectives += " }\\n";
|
|
1595
|
+
inLocationBlock = false;
|
|
1596
|
+
}
|
|
1597
|
+
currentPath = trimmed;
|
|
1598
|
+
// Global headers (/*) go at server level, others get location blocks
|
|
1599
|
+
if (currentPath !== "/*") {
|
|
1600
|
+
headerDirectives += \\\`\\n location \\\${currentPath} {\\n\\\`;
|
|
1601
|
+
inLocationBlock = true;
|
|
1602
|
+
}
|
|
1603
|
+
} else {
|
|
1604
|
+
// Header line (indented)
|
|
1605
|
+
const colonIdx = trimmed.indexOf(":");
|
|
1606
|
+
if (colonIdx === -1) continue;
|
|
1607
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
1608
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
1609
|
+
if (key && value) {
|
|
1610
|
+
if (currentPath === "/*") {
|
|
1611
|
+
headerDirectives += \\\` add_header \\\${key} "\\\${value}" always;\\n\\\`;
|
|
1612
|
+
} else {
|
|
1613
|
+
headerDirectives += \\\` add_header \\\${key} "\\\${value}" always;\\n\\\`;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Close final location block
|
|
1620
|
+
if (inLocationBlock) {
|
|
1621
|
+
headerDirectives += " }\\n";
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
nginxConfig = nginxConfig.replace(
|
|
1625
|
+
"# CUSTOM_HEADERS_PLACEHOLDER",
|
|
1626
|
+
headerDirectives
|
|
1627
|
+
);
|
|
1628
|
+
console.log("Processed _headers file");
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// ── Parse _redirects (Netlify-style) ───────────────────
|
|
1632
|
+
//
|
|
1633
|
+
// Format:
|
|
1634
|
+
// /old-path /new-path 301
|
|
1635
|
+
// /api/* https://api.example.com/:splat 200
|
|
1636
|
+
|
|
1637
|
+
const redirectsPath = path.join(outputDir, "_redirects");
|
|
1638
|
+
if (fs.existsSync(redirectsPath)) {
|
|
1639
|
+
const content = fs.readFileSync(redirectsPath, "utf-8");
|
|
1640
|
+
let redirectDirectives = "";
|
|
1641
|
+
|
|
1642
|
+
for (const line of content.split("\\n")) {
|
|
1643
|
+
const trimmed = line.trim();
|
|
1644
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1645
|
+
|
|
1646
|
+
const parts = trimmed.split(/\\s+/);
|
|
1647
|
+
if (parts.length < 2) continue;
|
|
1648
|
+
|
|
1649
|
+
const from = parts[0];
|
|
1650
|
+
const to = parts[1];
|
|
1651
|
+
const code = parts[2] || "301";
|
|
1652
|
+
|
|
1653
|
+
if (!from || !to) continue;
|
|
1654
|
+
|
|
1655
|
+
if (!from.includes("*") && !to.includes(":splat")) {
|
|
1656
|
+
// Simple redirect
|
|
1657
|
+
const nginxFlag = code === "301" ? "permanent" : "redirect";
|
|
1658
|
+
redirectDirectives += \\\` rewrite ^\\\${from}\\\\$ \\\${to} \\\${nginxFlag};\\n\\\`;
|
|
1659
|
+
} else {
|
|
1660
|
+
// Wildcard redirect
|
|
1661
|
+
const fromPattern = from.replace(/\\*/g, "(.*)");
|
|
1662
|
+
const toPattern = to.replace(/:splat/g, "\\$1");
|
|
1663
|
+
const nginxFlag = code === "301" ? "permanent" : "redirect";
|
|
1664
|
+
redirectDirectives += \\\` rewrite ^\\\${fromPattern}\\\\$ \\\${toPattern} \\\${nginxFlag};\\n\\\`;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
nginxConfig = nginxConfig.replace(
|
|
1669
|
+
"# CUSTOM_REDIRECTS_PLACEHOLDER",
|
|
1670
|
+
redirectDirectives
|
|
1671
|
+
);
|
|
1672
|
+
console.log("Processed _redirects file");
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// ── Write final config ─────────────────────────────────
|
|
1676
|
+
|
|
1677
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
1678
|
+
fs.writeFileSync(outFile, nginxConfig);
|
|
1679
|
+
console.log(\\\`Generated nginx config at \\\${outFile}\\\`);`;
|
|
1680
|
+
|
|
1681
|
+
//#endregion
|
|
1682
|
+
//#region src/commands/deploy.ts
|
|
1683
|
+
async function withRetry(fn, maxRetries = 3) {
|
|
1684
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
1685
|
+
return await fn();
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
const rateLimit = isRateLimitError(error);
|
|
1688
|
+
if (rateLimit && attempt < maxRetries) {
|
|
1689
|
+
const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
|
|
1690
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
throw error;
|
|
1694
|
+
}
|
|
1695
|
+
throw new Error("Max retries exceeded");
|
|
1696
|
+
}
|
|
1697
|
+
function detectPackageManager() {
|
|
1698
|
+
if (existsSync(resolve(process.cwd(), "bun.lockb")) || existsSync(resolve(process.cwd(), "bun.lock"))) return "bun";
|
|
1699
|
+
if (existsSync(resolve(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
|
|
1700
|
+
if (existsSync(resolve(process.cwd(), "yarn.lock"))) return "yarn";
|
|
1701
|
+
return "npm";
|
|
1702
|
+
}
|
|
1703
|
+
function detectNodeVersion() {
|
|
1704
|
+
try {
|
|
1705
|
+
const pkgPath = resolve(process.cwd(), "package.json");
|
|
1706
|
+
if (existsSync(pkgPath)) {
|
|
1707
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1708
|
+
if (pkg.engines?.node) {
|
|
1709
|
+
const match = pkg.engines.node.match(/(\d+)/);
|
|
1710
|
+
if (match) return match[1];
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
} catch {}
|
|
1714
|
+
return "20";
|
|
1715
|
+
}
|
|
1716
|
+
function prepareExtraFiles(detection, serviceConfig) {
|
|
1717
|
+
if (existsSync(resolve(process.cwd(), "Dockerfile"))) return [];
|
|
1718
|
+
const rootDir = serviceConfig?.rootDirectory || ".";
|
|
1719
|
+
const serviceDockerfilePath = resolve(process.cwd(), rootDir, "Dockerfile");
|
|
1720
|
+
if (rootDir !== "." && existsSync(serviceDockerfilePath)) return [{
|
|
1721
|
+
name: "Dockerfile",
|
|
1722
|
+
content: readFileSync(serviceDockerfilePath, "utf-8")
|
|
1723
|
+
}];
|
|
1724
|
+
const fw = detection.framework;
|
|
1725
|
+
const pm = detectPackageManager();
|
|
1726
|
+
const nodeVersion = detectNodeVersion();
|
|
1727
|
+
const type = serviceConfig?.type?.toUpperCase() ?? fw?.type ?? "WEB";
|
|
1728
|
+
const files = [{
|
|
1729
|
+
name: "Dockerfile",
|
|
1730
|
+
content: generateDockerfile({
|
|
1731
|
+
serviceType: type,
|
|
1732
|
+
nodeVersion,
|
|
1733
|
+
pm,
|
|
1734
|
+
buildCommand: serviceConfig?.buildCommand ?? fw?.buildCommand ?? `${pm} run build`,
|
|
1735
|
+
startCommand: serviceConfig?.startCommand ?? fw?.startCommand ?? void 0,
|
|
1736
|
+
outputDir: serviceConfig?.outputDir ?? fw?.outputDir ?? void 0,
|
|
1737
|
+
rootDirectory: serviceConfig?.rootDirectory,
|
|
1738
|
+
port: serviceConfig?.port ?? fw?.port ?? 3e3
|
|
1739
|
+
})
|
|
1740
|
+
}];
|
|
1741
|
+
if (type === "STATIC") files.push({
|
|
1742
|
+
name: "nginx.conf",
|
|
1743
|
+
content: NGINX_CONF
|
|
1744
|
+
}, {
|
|
1745
|
+
name: "generate-nginx-config.cjs",
|
|
1746
|
+
content: GENERATE_NGINX_CONFIG_CJS
|
|
1747
|
+
});
|
|
1748
|
+
return files;
|
|
1749
|
+
}
|
|
1750
|
+
function computeExtraFilesForServices(services) {
|
|
1751
|
+
const velozConfig = loadConfig();
|
|
1752
|
+
return services.map((svc) => {
|
|
1753
|
+
let serviceConf;
|
|
1754
|
+
if (velozConfig) {
|
|
1755
|
+
for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === svc.serviceId) {
|
|
1756
|
+
const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
|
|
1757
|
+
serviceConf = {
|
|
1758
|
+
type: merged.type,
|
|
1759
|
+
buildCommand: merged.build?.command ?? void 0,
|
|
1760
|
+
startCommand: merged.runtime?.command ?? void 0,
|
|
1761
|
+
port: merged.runtime?.port ?? void 0,
|
|
1762
|
+
rootDirectory: merged.root,
|
|
1763
|
+
outputDir: merged.build?.outputDir ?? void 0
|
|
1764
|
+
};
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const detection = detectLocalRepo();
|
|
1769
|
+
return {
|
|
1770
|
+
...svc,
|
|
1771
|
+
extraFiles: prepareExtraFiles(detection, serviceConf)
|
|
1772
|
+
};
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
async function triggerDeploy(serviceId, serviceName) {
|
|
1776
|
+
const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
|
|
1777
|
+
try {
|
|
1778
|
+
const sizeInBytes = await calculateDirectorySize(process.cwd());
|
|
1779
|
+
const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
|
|
1780
|
+
if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
|
|
1781
|
+
const client = await getClient();
|
|
1782
|
+
const authConfig = await requireAuth();
|
|
1783
|
+
const velozConfig = loadConfig();
|
|
1784
|
+
let serviceConf;
|
|
1785
|
+
if (velozConfig) {
|
|
1786
|
+
for (const [, svc] of Object.entries(velozConfig.services)) if (svc.id === serviceId) {
|
|
1787
|
+
const merged = mergeServiceWithDefaults(svc, velozConfig.defaults);
|
|
1788
|
+
serviceConf = {
|
|
1789
|
+
type: merged.type,
|
|
1790
|
+
buildCommand: merged.build?.command ?? void 0,
|
|
1791
|
+
startCommand: merged.runtime?.command ?? void 0,
|
|
1792
|
+
port: merged.runtime?.port ?? void 0,
|
|
1793
|
+
rootDirectory: merged.root,
|
|
1794
|
+
outputDir: merged.build?.outputDir ?? void 0
|
|
1795
|
+
};
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const extraFiles = prepareExtraFiles(detectLocalRepo(), serviceConf);
|
|
1800
|
+
spinUpload.text = "Iniciando deploy...";
|
|
1801
|
+
const deployment = await withRetry(() => client.deployments.create({ serviceId }));
|
|
1802
|
+
spinUpload.text = "Fazendo upload do código...";
|
|
1803
|
+
await withRetry(() => uploadSource(authConfig.apiUrl, deployment.id, process.cwd(), authConfig.apiKey, extraFiles));
|
|
1804
|
+
spinUpload.stop();
|
|
1805
|
+
success("Deploy iniciado com sucesso!");
|
|
1806
|
+
await streamDeploymentLogs(client, deployment.id, serviceId, serviceName);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
spinUpload.stop();
|
|
1809
|
+
handleError(error);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
async function findServicesFromConfig() {
|
|
1813
|
+
const config = loadConfig();
|
|
1814
|
+
if (!config) return [];
|
|
1815
|
+
const services = [];
|
|
1816
|
+
for (const [key, serviceConfig] of Object.entries(config.services)) services.push({
|
|
1817
|
+
path: resolve(process.cwd(), serviceConfig.root),
|
|
1818
|
+
serviceId: serviceConfig.id,
|
|
1819
|
+
projectId: config.project.id,
|
|
1820
|
+
serviceName: serviceConfig.name,
|
|
1821
|
+
projectName: config.project.name,
|
|
1822
|
+
key
|
|
1823
|
+
});
|
|
1824
|
+
return services;
|
|
1825
|
+
}
|
|
1826
|
+
function readLocalFile(path) {
|
|
1827
|
+
const fullPath = resolve(process.cwd(), path);
|
|
1828
|
+
if (!existsSync(fullPath)) return null;
|
|
1829
|
+
try {
|
|
1830
|
+
return readFileSync(fullPath, "utf-8");
|
|
1831
|
+
} catch {
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function printSummary(settings) {
|
|
1836
|
+
console.log();
|
|
1837
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
1838
|
+
console.log(` ${chalk.bold("Nome:")} ${settings.name}`);
|
|
1839
|
+
console.log(` ${chalk.bold("Tipo:")} ${settings.type === "WEB" ? "Serviço Web" : "Site Estático"}`);
|
|
1840
|
+
console.log(` ${chalk.bold("Branch:")} ${settings.branch}`);
|
|
1841
|
+
if (settings.framework) console.log(` ${chalk.bold("Framework:")} ${settings.framework}`);
|
|
1842
|
+
if (settings.packageManager) console.log(` ${chalk.bold("Package Mgr:")} ${settings.packageManager}`);
|
|
1843
|
+
if (settings.buildCommand) console.log(` ${chalk.bold("Build:")} ${settings.buildCommand}`);
|
|
1844
|
+
if (settings.startCommand) console.log(` ${chalk.bold("Start:")} ${settings.startCommand}`);
|
|
1845
|
+
if (settings.outputDir) console.log(` ${chalk.bold("Output:")} ${settings.outputDir}`);
|
|
1846
|
+
if (settings.port) console.log(` ${chalk.bold("Porta:")} ${settings.port}`);
|
|
1847
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
1848
|
+
console.log();
|
|
1849
|
+
}
|
|
1850
|
+
function detectLocalRepo(basePath = ".") {
|
|
1851
|
+
const files = {};
|
|
1852
|
+
const pkgJson = readLocalFile(join(basePath, "package.json"));
|
|
1853
|
+
if (pkgJson) files["package.json"] = pkgJson;
|
|
1854
|
+
const envExample = readLocalFile(join(basePath, ".env.example"));
|
|
1855
|
+
if (envExample) files[".env.example"] = envExample;
|
|
1856
|
+
else {
|
|
1857
|
+
const envSample = readLocalFile(join(basePath, ".env.sample"));
|
|
1858
|
+
if (envSample) files[".env.sample"] = envSample;
|
|
1859
|
+
else {
|
|
1860
|
+
const envFile = readLocalFile(join(basePath, ".env"));
|
|
1861
|
+
if (envFile) files[".env"] = envFile;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const pnpmWs = readLocalFile(join(basePath, "pnpm-workspace.yaml"));
|
|
1865
|
+
if (pnpmWs) files["pnpm-workspace.yaml"] = pnpmWs;
|
|
1866
|
+
for (const lf of [
|
|
1867
|
+
"pnpm-lock.yaml",
|
|
1868
|
+
"yarn.lock",
|
|
1869
|
+
"bun.lock",
|
|
1870
|
+
"bun.lockb",
|
|
1871
|
+
"package-lock.json"
|
|
1872
|
+
]) if (existsSync(resolve(process.cwd(), basePath, lf))) files[lf] = "";
|
|
1873
|
+
const workspacePatterns = [];
|
|
1874
|
+
if (pkgJson) try {
|
|
1875
|
+
const pkg = JSON.parse(pkgJson);
|
|
1876
|
+
if (pkg.workspaces) {
|
|
1877
|
+
const ws = pkg.workspaces;
|
|
1878
|
+
if (Array.isArray(ws)) workspacePatterns.push(...ws);
|
|
1879
|
+
else if (typeof ws === "object" && ws !== null && "packages" in ws && Array.isArray(ws.packages)) workspacePatterns.push(...ws.packages);
|
|
1880
|
+
}
|
|
1881
|
+
} catch {}
|
|
1882
|
+
if (pnpmWs) {
|
|
1883
|
+
const lines = pnpmWs.split("\n");
|
|
1884
|
+
let inPackages = false;
|
|
1885
|
+
for (const line of lines) {
|
|
1886
|
+
if (line.match(/^packages\s*:/)) {
|
|
1887
|
+
inPackages = true;
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
if (inPackages) {
|
|
1891
|
+
const m = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
|
|
1892
|
+
if (m) workspacePatterns.push(m[1]);
|
|
1893
|
+
else if (line.match(/^\S/)) break;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
for (const pattern of workspacePatterns) {
|
|
1898
|
+
const hasGlob = /\/?\*\*?$/.test(pattern);
|
|
1899
|
+
const base = pattern.replace(/\/?\*\*?$/, "");
|
|
1900
|
+
if (hasGlob) {
|
|
1901
|
+
const dirPath = resolve(process.cwd(), base);
|
|
1902
|
+
if (!existsSync(dirPath)) continue;
|
|
1903
|
+
try {
|
|
1904
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
1905
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
1906
|
+
const nestedPkg = readLocalFile(join(basePath, base, entry.name, "package.json"));
|
|
1907
|
+
if (nestedPkg) files[`${base}/${entry.name}/package.json`] = nestedPkg;
|
|
1908
|
+
}
|
|
1909
|
+
} catch {}
|
|
1910
|
+
} else {
|
|
1911
|
+
const nestedPkg = readLocalFile(join(basePath, base, "package.json"));
|
|
1912
|
+
if (nestedPkg) files[`${base}/package.json`] = nestedPkg;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return analyzeRepo(files);
|
|
1916
|
+
}
|
|
1917
|
+
async function promptEnvVars(client, serviceId, detectedVars) {
|
|
1918
|
+
if (detectedVars.length === 0) return;
|
|
1919
|
+
console.log(chalk.cyan(`\n📝 ${detectedVars.length} variável(is) de ambiente detectada(s):\n`));
|
|
1920
|
+
for (const v of detectedVars) console.log(` • ${v.key}`);
|
|
1921
|
+
console.log();
|
|
1922
|
+
if (!await promptConfirm("Deseja preencher as variáveis agora?", false)) return;
|
|
1923
|
+
const vars = {};
|
|
1924
|
+
for (const v of detectedVars) {
|
|
1925
|
+
const finalVal = await prompt(` ${chalk.bold(v.key)}:${v.value ? chalk.dim(` (${v.value})`) : ""}`) || v.value;
|
|
1926
|
+
if (finalVal) vars[v.key] = finalVal;
|
|
1927
|
+
}
|
|
1928
|
+
const filled = Object.keys(vars).length;
|
|
1929
|
+
if (filled > 0) {
|
|
1930
|
+
const spinVars = spinner("Definindo variáveis de ambiente...");
|
|
1931
|
+
await withRetry(() => client.envVars.setBulk({
|
|
1932
|
+
serviceId,
|
|
1933
|
+
vars
|
|
1934
|
+
}));
|
|
1935
|
+
spinVars.stop();
|
|
1936
|
+
success(`${filled} variável(is) definida(s).`);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
1940
|
+
const branch = getGitBranch();
|
|
1941
|
+
const spinDetect = spinner("Detectando framework...");
|
|
1942
|
+
const detection = detectLocalRepo();
|
|
1943
|
+
spinDetect.stop();
|
|
1944
|
+
const pm = detection.packageManager;
|
|
1945
|
+
if (detection.isMonorepo && detection.monorepoApps.length > 0) {
|
|
1946
|
+
info(`Monorepo detectado (${pm})`);
|
|
1947
|
+
const selectedPaths = await promptMultiSelect("Quais apps deseja fazer o deploy?", detection.monorepoApps.map((app) => ({
|
|
1948
|
+
label: `${app.name}${app.framework ? ` (${app.framework.label})` : ""} — ${app.path}`,
|
|
1949
|
+
value: app.path
|
|
1950
|
+
})));
|
|
1951
|
+
const selectedApps = detection.monorepoApps.filter((a) => selectedPaths.includes(a.path));
|
|
1952
|
+
for (const app of selectedApps) {
|
|
1953
|
+
const fw$1 = app.framework;
|
|
1954
|
+
console.log(chalk.cyan(`\n── ${app.name} ──`));
|
|
1955
|
+
printSummary({
|
|
1956
|
+
name: app.name,
|
|
1957
|
+
type: fw$1?.type ?? "WEB",
|
|
1958
|
+
rootDir: app.path,
|
|
1959
|
+
branch,
|
|
1960
|
+
framework: fw$1?.label ?? null,
|
|
1961
|
+
packageManager: pm,
|
|
1962
|
+
buildCommand: fw$1?.buildCommand ?? null,
|
|
1963
|
+
startCommand: fw$1?.startCommand ?? null,
|
|
1964
|
+
outputDir: fw$1?.outputDir ?? null,
|
|
1965
|
+
port: fw$1?.port ?? 3e3
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
if (!await promptConfirm("Confirmar e fazer deploy?")) for (const app of selectedApps) {
|
|
1969
|
+
const fw$1 = app.framework;
|
|
1970
|
+
console.log(chalk.cyan(`\n── Editar: ${app.name} ──`));
|
|
1971
|
+
const newBuild = await prompt(` Build command: ${chalk.dim(`(${fw$1?.buildCommand ?? "—"})`)}`);
|
|
1972
|
+
if (newBuild && fw$1) fw$1.buildCommand = newBuild;
|
|
1973
|
+
const newStart = await prompt(` Start command: ${chalk.dim(`(${fw$1?.startCommand ?? "—"})`)}`);
|
|
1974
|
+
if (newStart && fw$1) fw$1.startCommand = newStart;
|
|
1975
|
+
const newPort = await prompt(` Port: ${chalk.dim(`(${fw$1?.port ?? 3e3})`)}`);
|
|
1976
|
+
if (newPort && fw$1) fw$1.port = parseInt(newPort, 10) || fw$1.port;
|
|
1977
|
+
}
|
|
1978
|
+
const config = {
|
|
1979
|
+
version: "1.0",
|
|
1980
|
+
project: {
|
|
1981
|
+
id: projectId,
|
|
1982
|
+
name: projectName
|
|
1983
|
+
},
|
|
1984
|
+
services: {},
|
|
1985
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
1986
|
+
};
|
|
1987
|
+
const createdServices = [];
|
|
1988
|
+
for (const app of selectedApps) {
|
|
1989
|
+
const fw$1 = app.framework;
|
|
1990
|
+
console.log(chalk.cyan(`\n── Criando serviço: ${app.name} ──\n`));
|
|
1991
|
+
const spinService$1 = spinner(`Criando serviço ${chalk.bold(app.name)}...`);
|
|
1992
|
+
const service$1 = await withRetry(() => client.services.create({
|
|
1993
|
+
projectId,
|
|
1994
|
+
name: app.name,
|
|
1995
|
+
type: fw$1?.type ?? "WEB",
|
|
1996
|
+
branch,
|
|
1997
|
+
rootDirectory: app.path,
|
|
1998
|
+
buildCommand: fw$1?.buildCommand ?? void 0,
|
|
1999
|
+
startCommand: fw$1?.startCommand ?? void 0,
|
|
2000
|
+
port: fw$1?.port ?? 3e3
|
|
2001
|
+
}));
|
|
2002
|
+
spinService$1.stop();
|
|
2003
|
+
success(`Serviço criado: ${chalk.bold(service$1.name)}`);
|
|
2004
|
+
createdServices.push({
|
|
2005
|
+
service: service$1,
|
|
2006
|
+
app
|
|
2007
|
+
});
|
|
2008
|
+
config.services[app.path] = {
|
|
2009
|
+
id: service$1.id,
|
|
2010
|
+
name: service$1.name,
|
|
2011
|
+
type: (fw$1?.type ?? "web").toLowerCase(),
|
|
2012
|
+
root: app.path,
|
|
2013
|
+
branch,
|
|
2014
|
+
build: fw$1?.buildCommand || fw$1?.outputDir ? {
|
|
2015
|
+
command: fw$1?.buildCommand ?? void 0,
|
|
2016
|
+
outputDir: fw$1?.outputDir ?? void 0
|
|
2017
|
+
} : void 0,
|
|
2018
|
+
runtime: {
|
|
2019
|
+
command: fw$1?.startCommand ?? void 0,
|
|
2020
|
+
port: fw$1?.port ?? 3e3
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
saveConfig(config);
|
|
2025
|
+
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2026
|
+
for (const { service: service$1, app } of createdServices) {
|
|
2027
|
+
console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
|
|
2028
|
+
const serviceDetection = detectLocalRepo(app.path);
|
|
2029
|
+
await promptEnvVars(client, service$1.id, serviceDetection.envVars);
|
|
2030
|
+
}
|
|
2031
|
+
await deployServicesInParallel(client, createdServices.map(({ service: service$1, app }) => ({
|
|
2032
|
+
serviceId: service$1.id,
|
|
2033
|
+
serviceName: app.name,
|
|
2034
|
+
path: resolve(process.cwd(), app.path),
|
|
2035
|
+
extraFiles: prepareExtraFiles(detectLocalRepo(app.path), {
|
|
2036
|
+
type: app.framework?.type,
|
|
2037
|
+
buildCommand: app.framework?.buildCommand ?? void 0,
|
|
2038
|
+
startCommand: app.framework?.startCommand ?? void 0,
|
|
2039
|
+
port: app.framework?.port ?? void 0,
|
|
2040
|
+
rootDirectory: app.path,
|
|
2041
|
+
outputDir: app.framework?.outputDir ?? void 0
|
|
2042
|
+
})
|
|
2043
|
+
})));
|
|
2044
|
+
return createdServices[createdServices.length - 1]?.service.id || "";
|
|
2045
|
+
}
|
|
2046
|
+
const fw = detection.framework;
|
|
2047
|
+
const settings = {
|
|
2048
|
+
name: repoName,
|
|
2049
|
+
type: fw?.type ?? "WEB",
|
|
2050
|
+
rootDir: ".",
|
|
2051
|
+
branch,
|
|
2052
|
+
framework: fw?.label ?? null,
|
|
2053
|
+
packageManager: pm,
|
|
2054
|
+
buildCommand: fw?.buildCommand ?? null,
|
|
2055
|
+
startCommand: fw?.startCommand ?? null,
|
|
2056
|
+
outputDir: fw?.outputDir ?? null,
|
|
2057
|
+
port: fw?.port ?? 3e3
|
|
2058
|
+
};
|
|
2059
|
+
if (fw) info(`Framework detectado: ${chalk.bold(fw.label)}`);
|
|
2060
|
+
printSummary(settings);
|
|
2061
|
+
if (!await promptConfirm("Confirmar e fazer deploy?")) {
|
|
2062
|
+
const newName = await prompt(`Nome do serviço: ${chalk.dim(`(${settings.name})`)}`);
|
|
2063
|
+
if (newName) settings.name = newName;
|
|
2064
|
+
settings.type = await promptSelect("Tipo de serviço:", [{
|
|
2065
|
+
label: "Serviço Web",
|
|
2066
|
+
value: "WEB"
|
|
2067
|
+
}, {
|
|
2068
|
+
label: "Site Estático",
|
|
2069
|
+
value: "STATIC"
|
|
2070
|
+
}]);
|
|
2071
|
+
const newBuild = await prompt(`Build command: ${chalk.dim(`(${settings.buildCommand ?? "—"})`)}`);
|
|
2072
|
+
if (newBuild) settings.buildCommand = newBuild;
|
|
2073
|
+
const newStart = await prompt(`Start command: ${chalk.dim(`(${settings.startCommand ?? "—"})`)}`);
|
|
2074
|
+
if (newStart) settings.startCommand = newStart;
|
|
2075
|
+
const newPort = await prompt(`Port: ${chalk.dim(`(${settings.port})`)}`);
|
|
2076
|
+
if (newPort) settings.port = parseInt(newPort, 10) || settings.port;
|
|
2077
|
+
}
|
|
2078
|
+
const spinService = spinner("Criando serviço...");
|
|
2079
|
+
const service = await withRetry(() => client.services.create({
|
|
2080
|
+
projectId,
|
|
2081
|
+
name: settings.name,
|
|
2082
|
+
type: settings.type,
|
|
2083
|
+
branch,
|
|
2084
|
+
rootDirectory: settings.rootDir,
|
|
2085
|
+
buildCommand: settings.buildCommand ?? void 0,
|
|
2086
|
+
startCommand: settings.startCommand ?? void 0,
|
|
2087
|
+
port: settings.port
|
|
2088
|
+
}));
|
|
2089
|
+
spinService.stop();
|
|
2090
|
+
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2091
|
+
await promptEnvVars(client, service.id, detection.envVars);
|
|
2092
|
+
saveConfig({
|
|
2093
|
+
version: "1.0",
|
|
2094
|
+
project: {
|
|
2095
|
+
id: projectId,
|
|
2096
|
+
name: projectName
|
|
2097
|
+
},
|
|
2098
|
+
services: { main: {
|
|
2099
|
+
id: service.id,
|
|
2100
|
+
name: service.name,
|
|
2101
|
+
type: settings.type.toLowerCase(),
|
|
2102
|
+
root: settings.rootDir,
|
|
2103
|
+
branch,
|
|
2104
|
+
build: settings.buildCommand || settings.outputDir ? {
|
|
2105
|
+
command: settings.buildCommand ?? void 0,
|
|
2106
|
+
outputDir: settings.outputDir ?? void 0
|
|
2107
|
+
} : void 0,
|
|
2108
|
+
runtime: {
|
|
2109
|
+
command: settings.startCommand ?? void 0,
|
|
2110
|
+
port: settings.port
|
|
2111
|
+
}
|
|
2112
|
+
} },
|
|
2113
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2114
|
+
});
|
|
2115
|
+
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2116
|
+
return service.id;
|
|
2117
|
+
}
|
|
2118
|
+
const deployCommand = new Command("deploy").description("Fazer deploy do serviço (auto-detecta projeto pelo git)").option("-a, --all", "Deploy todos os serviços do monorepo").option("--service <service>", "Deploy de um serviço específico (chave ou nome)").option("-v, --verbose", "Mostrar logs detalhados do servidor").action(async (options) => {
|
|
2119
|
+
if (options.verbose) process.env.VELOZ_VERBOSE = "true";
|
|
2120
|
+
try {
|
|
2121
|
+
await requireAuth();
|
|
2122
|
+
const configuredServices = await findServicesFromConfig();
|
|
2123
|
+
if (configuredServices.length > 0) {
|
|
2124
|
+
if (options.service) {
|
|
2125
|
+
const found = configuredServices.find((s) => s.key === options.service || s.serviceName.toLowerCase() === options.service.toLowerCase() || s.serviceId === options.service);
|
|
2126
|
+
if (!found) {
|
|
2127
|
+
const available = configuredServices.map((s) => ` • ${s.key} (${s.serviceName})`).join("\n");
|
|
2128
|
+
console.error(chalk.red(`\n✗ Serviço '${options.service}' não encontrado.\n\nServiços disponíveis:\n${available}`));
|
|
2129
|
+
process.exit(1);
|
|
2130
|
+
}
|
|
2131
|
+
await triggerDeploy(found.serviceId, found.serviceName);
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
if (options.all || configuredServices.length === 1) {
|
|
2135
|
+
if (configuredServices.length > 1) {
|
|
2136
|
+
console.log(chalk.cyan(`\n🚀 Fazendo deploy de ${configuredServices.length} serviço(s):\n`));
|
|
2137
|
+
for (const service of configuredServices) {
|
|
2138
|
+
const relPath = relative(process.cwd(), service.path) || ".";
|
|
2139
|
+
console.log(` • ${chalk.bold(service.serviceName)} ${chalk.dim(`(${relPath})`)}`);
|
|
2140
|
+
}
|
|
2141
|
+
console.log();
|
|
2142
|
+
if (!await promptConfirm("Confirmar deploy de todos os serviços?")) {
|
|
2143
|
+
info("Deploy cancelado.");
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
if (configuredServices.length === 1) await triggerDeploy(configuredServices[0].serviceId, configuredServices[0].serviceName);
|
|
2148
|
+
else {
|
|
2149
|
+
const servicesWithExtraFiles = computeExtraFilesForServices(configuredServices);
|
|
2150
|
+
await deployServicesInParallel(await getClient(), servicesWithExtraFiles);
|
|
2151
|
+
}
|
|
2152
|
+
return;
|
|
2153
|
+
} else {
|
|
2154
|
+
console.log(chalk.bold("\n📦 Serviços disponíveis:\n"));
|
|
2155
|
+
const selectedServiceIds = await promptMultiSelect("Quais serviços deseja fazer deploy?", configuredServices.map((s) => {
|
|
2156
|
+
const relPath = relative(process.cwd(), s.path) || ".";
|
|
2157
|
+
return {
|
|
2158
|
+
label: `${s.serviceName} ${chalk.dim(`(${relPath})`)}`,
|
|
2159
|
+
value: s.serviceId
|
|
2160
|
+
};
|
|
2161
|
+
}));
|
|
2162
|
+
const selectedServices = configuredServices.filter((s) => selectedServiceIds.includes(s.serviceId));
|
|
2163
|
+
if (selectedServices.length === 0) {
|
|
2164
|
+
info("Nenhum serviço selecionado.");
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
if (selectedServices.length === 1) await triggerDeploy(selectedServices[0].serviceId, selectedServices[0].serviceName);
|
|
2168
|
+
else {
|
|
2169
|
+
const servicesWithExtraFiles = computeExtraFilesForServices(selectedServices);
|
|
2170
|
+
await deployServicesInParallel(await getClient(), servicesWithExtraFiles);
|
|
2171
|
+
}
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (!isGitRepo()) {
|
|
2176
|
+
console.error(chalk.red("\n✗ Este diretório não é um repositório git. Inicialize com `git init` e adicione um remote."));
|
|
2177
|
+
process.exit(1);
|
|
2178
|
+
}
|
|
2179
|
+
info("Detectando repositório git...");
|
|
2180
|
+
const remote = getGitRemote();
|
|
2181
|
+
if (!remote) {
|
|
2182
|
+
console.error(chalk.red("\n✗ Nenhum remote 'origin' encontrado. Adicione um remote git."));
|
|
2183
|
+
process.exit(1);
|
|
2184
|
+
}
|
|
2185
|
+
console.log(chalk.white(` ${chalk.bold("Repositório:")} ${remote.owner}/${remote.repo}`));
|
|
2186
|
+
const client = await getClient();
|
|
2187
|
+
const spin = spinner("Buscando projeto...");
|
|
2188
|
+
const project = await withRetry(() => client.projects.findByRepo({
|
|
2189
|
+
githubRepoOwner: remote.owner,
|
|
2190
|
+
githubRepoName: remote.repo
|
|
2191
|
+
}));
|
|
2192
|
+
spin.stop();
|
|
2193
|
+
if (project && project.services.length > 0) {
|
|
2194
|
+
info(`Projeto encontrado: ${chalk.bold(project.name)}`);
|
|
2195
|
+
let serviceId;
|
|
2196
|
+
let serviceName;
|
|
2197
|
+
if (project.services.length === 1) {
|
|
2198
|
+
const svc = project.services[0];
|
|
2199
|
+
serviceId = svc.id;
|
|
2200
|
+
serviceName = svc.name;
|
|
2201
|
+
} else {
|
|
2202
|
+
serviceId = await promptSelect("Selecione o serviço:", project.services.map((s) => ({
|
|
2203
|
+
label: `${s.name} (${s.type} — branch: ${s.branch})`,
|
|
2204
|
+
value: s.id
|
|
2205
|
+
})));
|
|
2206
|
+
serviceName = project.services.find((s) => s.id === serviceId)?.name ?? "";
|
|
2207
|
+
}
|
|
2208
|
+
const selectedService = project.services.find((s) => s.id === serviceId);
|
|
2209
|
+
saveConfig({
|
|
2210
|
+
version: "1.0",
|
|
2211
|
+
project: {
|
|
2212
|
+
id: project.id,
|
|
2213
|
+
name: project.name
|
|
2214
|
+
},
|
|
2215
|
+
services: { main: {
|
|
2216
|
+
id: serviceId,
|
|
2217
|
+
name: serviceName,
|
|
2218
|
+
type: selectedService?.type?.toLowerCase() ?? "web",
|
|
2219
|
+
root: ".",
|
|
2220
|
+
branch: selectedService?.branch
|
|
2221
|
+
} },
|
|
2222
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2223
|
+
});
|
|
2224
|
+
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2225
|
+
await triggerDeploy(serviceId);
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
if (project && project.services.length === 0) {
|
|
2229
|
+
info(`Projeto encontrado: ${chalk.bold(project.name)}`);
|
|
2230
|
+
info("Nenhum serviço configurado. Vamos criar um.");
|
|
2231
|
+
await triggerDeploy(await createServiceFlow(client, project.id, project.name, remote.repo));
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
info("Projeto não encontrado. Vamos criar um novo.");
|
|
2235
|
+
const projectName = await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
|
|
2236
|
+
const spinProject = spinner("Criando projeto...");
|
|
2237
|
+
const newProject = await withRetry(() => client.projects.create({
|
|
2238
|
+
name: projectName,
|
|
2239
|
+
githubRepoOwner: remote.owner,
|
|
2240
|
+
githubRepoName: remote.repo
|
|
2241
|
+
}));
|
|
2242
|
+
spinProject.stop();
|
|
2243
|
+
success(`Projeto criado: ${chalk.bold(newProject.name)}`);
|
|
2244
|
+
await triggerDeploy(await createServiceFlow(client, newProject.id, newProject.name, remote.repo));
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
handleError(error);
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
//#endregion
|
|
2251
|
+
//#region src/lib/service-resolver.ts
|
|
2252
|
+
const STATE_FILE = join(homedir(), ".veloz", "state.json");
|
|
2253
|
+
function loadState() {
|
|
2254
|
+
if (!existsSync(STATE_FILE)) return { projectDefaults: {} };
|
|
2255
|
+
try {
|
|
2256
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
2257
|
+
} catch {
|
|
2258
|
+
return { projectDefaults: {} };
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function saveState(state) {
|
|
2262
|
+
const dir = join(homedir(), ".veloz");
|
|
2263
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2264
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
2265
|
+
}
|
|
2266
|
+
function getDefaultServiceKey() {
|
|
2267
|
+
const config = loadConfig();
|
|
2268
|
+
if (!config) return null;
|
|
2269
|
+
return loadState().projectDefaults[config.project.id]?.currentService ?? null;
|
|
2270
|
+
}
|
|
2271
|
+
function setDefaultServiceKey(serviceKey) {
|
|
2272
|
+
const config = requireConfig();
|
|
2273
|
+
const state = loadState();
|
|
2274
|
+
state.projectDefaults[config.project.id] = { currentService: serviceKey };
|
|
2275
|
+
saveState(state);
|
|
2276
|
+
}
|
|
2277
|
+
const TAG_COLORS = [
|
|
2278
|
+
chalk.cyan,
|
|
2279
|
+
chalk.magenta,
|
|
2280
|
+
chalk.yellow,
|
|
2281
|
+
chalk.green,
|
|
2282
|
+
chalk.blue,
|
|
2283
|
+
chalk.red
|
|
2284
|
+
];
|
|
2285
|
+
function getServiceTag(name, maxLen, colorIndex) {
|
|
2286
|
+
const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
|
|
2287
|
+
return color(`[${name.padEnd(maxLen)}]`);
|
|
2288
|
+
}
|
|
2289
|
+
function getServiceHeader(name, colorIndex) {
|
|
2290
|
+
const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
|
|
2291
|
+
return color(`── ${name} ──`);
|
|
2292
|
+
}
|
|
2293
|
+
function findByFlag(entries, flag) {
|
|
2294
|
+
return entries.find(([key, svc]) => key === flag || svc.name.toLowerCase() === flag.toLowerCase() || svc.id === flag);
|
|
2295
|
+
}
|
|
2296
|
+
function throwNotFound(flag, entries) {
|
|
2297
|
+
const available = entries.map(([key, svc]) => ` • ${key} (${svc.name})`).join("\n");
|
|
2298
|
+
throw new Error(`Serviço '${flag}' não encontrado.\n\nServiços disponíveis:\n${available}`);
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Resolution order:
|
|
2302
|
+
* 1. Explicit `--service` flag → match by key, name, or id
|
|
2303
|
+
* 2. Single service in config → auto-select (zero friction)
|
|
2304
|
+
* 3. Default from `veloz use` → remembered choice
|
|
2305
|
+
* 4. Interactive prompt → last resort
|
|
2306
|
+
*/
|
|
2307
|
+
async function resolveService(serviceFlag) {
|
|
2308
|
+
const config = requireConfig();
|
|
2309
|
+
const entries = Object.entries(config.services);
|
|
2310
|
+
if (entries.length === 0) throw new Error("Nenhum serviço encontrado na configuração. Execute 'veloz deploy' para criar um.");
|
|
2311
|
+
if (serviceFlag) {
|
|
2312
|
+
const found = findByFlag(entries, serviceFlag);
|
|
2313
|
+
if (!found) throwNotFound(serviceFlag, entries);
|
|
2314
|
+
const [key, service] = found;
|
|
2315
|
+
return {
|
|
2316
|
+
key,
|
|
2317
|
+
service: mergeServiceWithDefaults(service, config.defaults),
|
|
2318
|
+
config
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
if (entries.length === 1) {
|
|
2322
|
+
const [key, service] = entries[0];
|
|
2323
|
+
return {
|
|
2324
|
+
key,
|
|
2325
|
+
service: mergeServiceWithDefaults(service, config.defaults),
|
|
2326
|
+
config
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
const defaultKey = getDefaultServiceKey();
|
|
2330
|
+
if (defaultKey && config.services[defaultKey]) {
|
|
2331
|
+
const service = config.services[defaultKey];
|
|
2332
|
+
return {
|
|
2333
|
+
key: defaultKey,
|
|
2334
|
+
service: mergeServiceWithDefaults(service, config.defaults),
|
|
2335
|
+
config
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
const selectedKey = await promptSelect("Qual serviço?", entries.map(([key, svc]) => ({
|
|
2339
|
+
label: `${svc.name} (${key})`,
|
|
2340
|
+
value: key
|
|
2341
|
+
})));
|
|
2342
|
+
return {
|
|
2343
|
+
key: selectedKey,
|
|
2344
|
+
service: mergeServiceWithDefaults(config.services[selectedKey], config.defaults),
|
|
2345
|
+
config
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
async function resolveServiceId(serviceFlag) {
|
|
2349
|
+
const { service } = await resolveService(serviceFlag);
|
|
2350
|
+
return service.id;
|
|
2351
|
+
}
|
|
2352
|
+
function resolveAllServices(serviceFlag) {
|
|
2353
|
+
const config = requireConfig();
|
|
2354
|
+
const entries = Object.entries(config.services);
|
|
2355
|
+
if (entries.length === 0) throw new Error("Nenhum serviço encontrado na configuração. Execute 'veloz deploy' para criar um.");
|
|
2356
|
+
if (serviceFlag) {
|
|
2357
|
+
const found = findByFlag(entries, serviceFlag);
|
|
2358
|
+
if (!found) throwNotFound(serviceFlag, entries);
|
|
2359
|
+
const [key, raw] = found;
|
|
2360
|
+
const service = mergeServiceWithDefaults(raw, config.defaults);
|
|
2361
|
+
return {
|
|
2362
|
+
services: [{
|
|
2363
|
+
key,
|
|
2364
|
+
service,
|
|
2365
|
+
index: 0
|
|
2366
|
+
}],
|
|
2367
|
+
config,
|
|
2368
|
+
maxNameLen: service.name.length
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
const services = entries.map(([key, raw], index) => ({
|
|
2372
|
+
key,
|
|
2373
|
+
service: mergeServiceWithDefaults(raw, config.defaults),
|
|
2374
|
+
index
|
|
2375
|
+
}));
|
|
2376
|
+
return {
|
|
2377
|
+
services,
|
|
2378
|
+
config,
|
|
2379
|
+
maxNameLen: Math.max(...services.map((s) => s.service.name.length))
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
//#endregion
|
|
2384
|
+
//#region src/commands/logs.ts
|
|
2385
|
+
function formatTime(timestamp) {
|
|
2386
|
+
return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
|
|
2387
|
+
}
|
|
2388
|
+
async function streamFollow(client, services, maxNameLen, tailLines) {
|
|
2389
|
+
const showTags = services.length > 1;
|
|
2390
|
+
const streams = services.map(async ({ service, index }) => {
|
|
2391
|
+
const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
|
|
2392
|
+
try {
|
|
2393
|
+
const stream = await client.logs.streamRuntime({
|
|
2394
|
+
serviceId: service.id,
|
|
2395
|
+
tailLines: Math.ceil(tailLines / services.length)
|
|
2396
|
+
});
|
|
2397
|
+
for await (const entry of stream) console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
|
|
2398
|
+
} catch {
|
|
2399
|
+
console.log(`${tag}${chalk.red("Erro ao conectar ao stream de logs")}`);
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
await Promise.allSettled(streams);
|
|
2403
|
+
}
|
|
2404
|
+
async function fetchRecent(client, services, maxNameLen, tailLines) {
|
|
2405
|
+
const showTags = services.length > 1;
|
|
2406
|
+
const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
|
|
2407
|
+
return (await client.logs.getRecent({
|
|
2408
|
+
serviceId: service.id,
|
|
2409
|
+
tailLines
|
|
2410
|
+
})).map((e) => ({
|
|
2411
|
+
...e,
|
|
2412
|
+
serviceIndex: index,
|
|
2413
|
+
serviceName: service.name
|
|
2414
|
+
}));
|
|
2415
|
+
}))).filter((r) => r.status === "fulfilled").flatMap((r) => r.value).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
2416
|
+
if (allEntries.length === 0) {
|
|
2417
|
+
info("Nenhum log encontrado.");
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
console.log();
|
|
2421
|
+
for (const entry of allEntries) {
|
|
2422
|
+
const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
|
|
2423
|
+
console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
|
|
2424
|
+
}
|
|
2425
|
+
console.log();
|
|
2426
|
+
info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
|
|
2427
|
+
}
|
|
2428
|
+
const logsCommand = new Command("logs").description("Visualizar logs dos serviços").option("-f, --follow", "Acompanhar logs em tempo real").option("-n, --tail <linhas>", "Número de linhas recentes", "50").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
|
|
2429
|
+
const spin = spinner("Carregando logs...");
|
|
2430
|
+
try {
|
|
2431
|
+
const { services, maxNameLen } = resolveAllServices(opts.service);
|
|
2432
|
+
const client = await getClient();
|
|
2433
|
+
const tailLines = parseInt(opts.tail, 10) || 50;
|
|
2434
|
+
if (opts.follow) {
|
|
2435
|
+
spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
|
|
2436
|
+
spin.stop();
|
|
2437
|
+
info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
|
|
2438
|
+
await streamFollow(client, services, maxNameLen, tailLines);
|
|
2439
|
+
} else {
|
|
2440
|
+
spin.stop();
|
|
2441
|
+
await fetchRecent(client, services, maxNameLen, tailLines);
|
|
2442
|
+
}
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
spin.stop();
|
|
2445
|
+
handleError(error);
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
//#endregion
|
|
2450
|
+
//#region src/commands/env.ts
|
|
2451
|
+
const envCommand = new Command("env").description("Gerenciar variáveis de ambiente");
|
|
2452
|
+
envCommand.command("list").alias("listar").description("Listar variáveis de ambiente").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
|
|
2453
|
+
const spin = spinner("Carregando variáveis de ambiente...");
|
|
2454
|
+
try {
|
|
2455
|
+
const { services } = resolveAllServices(opts.service);
|
|
2456
|
+
const client = await getClient();
|
|
2457
|
+
const showHeaders = services.length > 1;
|
|
2458
|
+
let totalVars = 0;
|
|
2459
|
+
for (const { service, index } of services) {
|
|
2460
|
+
const envVars = await client.envVars.list({ serviceId: service.id });
|
|
2461
|
+
totalVars += envVars.length;
|
|
2462
|
+
if (showHeaders) console.log(`\n${getServiceHeader(service.name, index)}`);
|
|
2463
|
+
if (envVars.length === 0) {
|
|
2464
|
+
info("Nenhuma variável de ambiente configurada.");
|
|
2465
|
+
continue;
|
|
2466
|
+
}
|
|
2467
|
+
printTable([
|
|
2468
|
+
"Chave",
|
|
2469
|
+
"Valor (mascarado)",
|
|
2470
|
+
"Atualizado em"
|
|
2471
|
+
], envVars.map((v) => [
|
|
2472
|
+
chalk.bold(v.key),
|
|
2473
|
+
chalk.dim(v.maskedValue),
|
|
2474
|
+
new Date(v.updatedAt).toLocaleDateString("pt-BR")
|
|
2475
|
+
]));
|
|
2476
|
+
}
|
|
2477
|
+
spin.stop();
|
|
2478
|
+
if (totalVars === 0 && !showHeaders) info("Nenhuma variável de ambiente configurada.");
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
spin.stop();
|
|
2481
|
+
handleError(error);
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
envCommand.command("set <pares...>").description("Definir variável de ambiente (formato: CHAVE=VALOR)").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (pares, opts) => {
|
|
2485
|
+
const spin = spinner("Salvando variáveis de ambiente...");
|
|
2486
|
+
try {
|
|
2487
|
+
const serviceId = await resolveServiceId(opts.service);
|
|
2488
|
+
const client = await getClient();
|
|
2489
|
+
for (const par of pares) {
|
|
2490
|
+
const eqIndex = par.indexOf("=");
|
|
2491
|
+
if (eqIndex === -1) {
|
|
2492
|
+
spin.stop();
|
|
2493
|
+
console.error(chalk.red(`\n✗ Formato inválido: "${par}". Use CHAVE=VALOR.`));
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
2496
|
+
const key = par.slice(0, eqIndex);
|
|
2497
|
+
const value = par.slice(eqIndex + 1);
|
|
2498
|
+
if (!key) {
|
|
2499
|
+
spin.stop();
|
|
2500
|
+
console.error(chalk.red("\n✗ Chave não pode estar vazia."));
|
|
2501
|
+
process.exit(1);
|
|
2502
|
+
}
|
|
2503
|
+
await client.envVars.set({
|
|
2504
|
+
serviceId,
|
|
2505
|
+
key,
|
|
2506
|
+
value
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
spin.stop();
|
|
2510
|
+
if (pares.length === 1) {
|
|
2511
|
+
const key = pares[0].slice(0, pares[0].indexOf("="));
|
|
2512
|
+
success(`Variável ${chalk.bold(key)} definida com sucesso.`);
|
|
2513
|
+
} else success(`${pares.length} variáveis definidas com sucesso.`);
|
|
2514
|
+
info("Faça um novo deploy para aplicar as alterações.");
|
|
2515
|
+
} catch (error) {
|
|
2516
|
+
spin.stop();
|
|
2517
|
+
handleError(error);
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
envCommand.command("delete <chave>").alias("deletar").description("Remover variável de ambiente").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (chave, opts) => {
|
|
2521
|
+
const spin = spinner("Removendo variável de ambiente...");
|
|
2522
|
+
try {
|
|
2523
|
+
const serviceId = await resolveServiceId(opts.service);
|
|
2524
|
+
await (await getClient()).envVars.delete({
|
|
2525
|
+
serviceId,
|
|
2526
|
+
key: chave
|
|
2527
|
+
});
|
|
2528
|
+
spin.stop();
|
|
2529
|
+
success(`Variável ${chalk.bold(chave)} removida com sucesso.`);
|
|
2530
|
+
info("Faça um novo deploy para aplicar as alterações.");
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
spin.stop();
|
|
2533
|
+
handleError(error);
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
envCommand.command("import [arquivo]").description("Importar variáveis de ambiente de um arquivo .env ou colar diretamente").option("-r, --replace", "Substituir todas as variáveis existentes").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (arquivo, options) => {
|
|
2537
|
+
try {
|
|
2538
|
+
const serviceId = await resolveServiceId(options.service);
|
|
2539
|
+
const client = await getClient();
|
|
2540
|
+
let envContent = "";
|
|
2541
|
+
if (arquivo) {
|
|
2542
|
+
const filePath = resolve(process.cwd(), arquivo);
|
|
2543
|
+
if (!existsSync(filePath)) {
|
|
2544
|
+
console.error(chalk.red(`\n✗ Arquivo não encontrado: ${arquivo}`));
|
|
2545
|
+
process.exit(1);
|
|
2546
|
+
}
|
|
2547
|
+
envContent = readFileSync(filePath, "utf-8");
|
|
2548
|
+
info(`Importando de ${chalk.bold(arquivo)}...`);
|
|
2549
|
+
} else {
|
|
2550
|
+
console.log(chalk.cyan("\n📋 Modo de colagem interativo"));
|
|
2551
|
+
console.log(chalk.dim("Cole seu conteúdo .env abaixo. Pressione Ctrl+D (ou Ctrl+Z no Windows) quando terminar:\n"));
|
|
2552
|
+
const rl = readline.createInterface({
|
|
2553
|
+
input: process.stdin,
|
|
2554
|
+
output: process.stdout
|
|
2555
|
+
});
|
|
2556
|
+
const lines$1 = [];
|
|
2557
|
+
await new Promise((resolve$1) => {
|
|
2558
|
+
rl.on("line", (line) => {
|
|
2559
|
+
lines$1.push(line);
|
|
2560
|
+
});
|
|
2561
|
+
rl.on("close", () => {
|
|
2562
|
+
resolve$1();
|
|
2563
|
+
});
|
|
2564
|
+
});
|
|
2565
|
+
envContent = lines$1.join("\n");
|
|
2566
|
+
}
|
|
2567
|
+
const envVars = {};
|
|
2568
|
+
const lines = envContent.split("\n");
|
|
2569
|
+
for (const line of lines) {
|
|
2570
|
+
const trimmed = line.trim();
|
|
2571
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2572
|
+
const eqIndex = trimmed.indexOf("=");
|
|
2573
|
+
if (eqIndex === -1) continue;
|
|
2574
|
+
let key = trimmed.slice(0, eqIndex).trim();
|
|
2575
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2576
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2577
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
|
|
2578
|
+
envVars[key] = value;
|
|
2579
|
+
}
|
|
2580
|
+
const varsCount = Object.keys(envVars).length;
|
|
2581
|
+
if (varsCount === 0) {
|
|
2582
|
+
console.error(chalk.yellow("⚠ Nenhuma variável válida encontrada."));
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
console.log(chalk.bold("\n📝 Variáveis a serem importadas:\n"));
|
|
2586
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
2587
|
+
const maskedValue = value.length > 20 ? value.slice(0, 20) + "..." : value;
|
|
2588
|
+
console.log(` ${chalk.green(key)}=${chalk.dim(maskedValue)}`);
|
|
2589
|
+
}
|
|
2590
|
+
console.log();
|
|
2591
|
+
if (options.replace) console.log(chalk.yellow("⚠ Modo de substituição: TODAS as variáveis existentes serão removidas!\n"));
|
|
2592
|
+
if (!await promptConfirm(`Importar ${varsCount} variável(is) de ambiente?`)) {
|
|
2593
|
+
info("Importação cancelada.");
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const spin = spinner("Importando variáveis de ambiente...");
|
|
2597
|
+
try {
|
|
2598
|
+
if (options.replace) {
|
|
2599
|
+
const existingVars = await client.envVars.list({ serviceId });
|
|
2600
|
+
for (const envVar of existingVars) await client.envVars.delete({
|
|
2601
|
+
serviceId,
|
|
2602
|
+
key: envVar.key
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
await client.envVars.setBulk({
|
|
2606
|
+
serviceId,
|
|
2607
|
+
vars: envVars
|
|
2608
|
+
});
|
|
2609
|
+
spin.stop();
|
|
2610
|
+
success(`${varsCount} variável(is) importada(s) com sucesso!`);
|
|
2611
|
+
info("Faça um novo deploy para aplicar as alterações.");
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
spin.stop();
|
|
2614
|
+
throw error;
|
|
2615
|
+
}
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
handleError(error);
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
envCommand.command("export [arquivo]").description("Exportar variáveis de ambiente para um arquivo .env").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (arquivo, opts) => {
|
|
2621
|
+
const spin = spinner("Carregando variáveis de ambiente...");
|
|
2622
|
+
try {
|
|
2623
|
+
const serviceId = await resolveServiceId(opts.service);
|
|
2624
|
+
const envVars = await (await getClient()).envVars.list({ serviceId });
|
|
2625
|
+
spin.stop();
|
|
2626
|
+
if (envVars.length === 0) {
|
|
2627
|
+
info("Nenhuma variável de ambiente para exportar.");
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
const envContent = envVars.map((v) => `${v.key}=${v.maskedValue}`).join("\n");
|
|
2631
|
+
if (arquivo) {
|
|
2632
|
+
writeFileSync(resolve(process.cwd(), arquivo), envContent + "\n", "utf-8");
|
|
2633
|
+
success(`Variáveis exportadas para ${chalk.bold(arquivo)}`);
|
|
2634
|
+
console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
|
|
2635
|
+
} else {
|
|
2636
|
+
console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
|
|
2637
|
+
console.log(envContent);
|
|
2638
|
+
console.log();
|
|
2639
|
+
}
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
spin.stop();
|
|
2642
|
+
handleError(error);
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
|
|
2646
|
+
//#endregion
|
|
2647
|
+
//#region src/commands/domains.ts
|
|
2648
|
+
const tlsStatusLabels = {
|
|
2649
|
+
PENDING: "Pendente",
|
|
2650
|
+
PROVISIONING: "Provisionando",
|
|
2651
|
+
ACTIVE: "Ativo",
|
|
2652
|
+
FAILED: "Falhou"
|
|
2653
|
+
};
|
|
2654
|
+
const domainsCommand = new Command("domains").alias("dominios").description("Gerenciar domínios personalizados");
|
|
2655
|
+
domainsCommand.command("list").alias("listar").description("Listar domínios dos serviços").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
|
|
2656
|
+
const spin = spinner("Carregando domínios...");
|
|
2657
|
+
try {
|
|
2658
|
+
const { services } = resolveAllServices(opts.service);
|
|
2659
|
+
const client = await getClient();
|
|
2660
|
+
const showHeaders = services.length > 1;
|
|
2661
|
+
let totalDomains = 0;
|
|
2662
|
+
for (const { service, index } of services) {
|
|
2663
|
+
const domains = await client.domains.list({ serviceId: service.id });
|
|
2664
|
+
totalDomains += domains.length;
|
|
2665
|
+
if (showHeaders) console.log(`\n${getServiceHeader(service.name, index)}`);
|
|
2666
|
+
if (domains.length === 0) {
|
|
2667
|
+
info("Nenhum domínio configurado.");
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
printTable([
|
|
2671
|
+
"ID",
|
|
2672
|
+
"Domínio",
|
|
2673
|
+
"Verificado",
|
|
2674
|
+
"TLS",
|
|
2675
|
+
"Tipo"
|
|
2676
|
+
], domains.map((d) => [
|
|
2677
|
+
d.id.slice(0, 8),
|
|
2678
|
+
chalk.bold(d.domain),
|
|
2679
|
+
d.verified ? chalk.green("✓ Sim") : chalk.yellow("✗ Não"),
|
|
2680
|
+
tlsStatusLabels[d.tlsStatus] ?? d.tlsStatus,
|
|
2681
|
+
d.isAutoGenerated ? "Auto" : "Personalizado"
|
|
2682
|
+
]));
|
|
2683
|
+
}
|
|
2684
|
+
spin.stop();
|
|
2685
|
+
if (totalDomains === 0 && !showHeaders) info("Nenhum domínio configurado.");
|
|
2686
|
+
} catch (error) {
|
|
2687
|
+
spin.stop();
|
|
2688
|
+
handleError(error);
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
domainsCommand.command("add <dominio>").alias("adicionar").description("Adicionar domínio personalizado").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (dominio, opts) => {
|
|
2692
|
+
const spin = spinner("Adicionando domínio...");
|
|
2693
|
+
try {
|
|
2694
|
+
const serviceId = await resolveServiceId(opts.service);
|
|
2695
|
+
const result = await (await getClient()).domains.add({
|
|
2696
|
+
serviceId,
|
|
2697
|
+
domain: dominio
|
|
2698
|
+
});
|
|
2699
|
+
spin.stop();
|
|
2700
|
+
success(`Domínio ${chalk.bold(dominio)} adicionado com sucesso!`);
|
|
2701
|
+
console.log();
|
|
2702
|
+
console.log(chalk.yellow.bold(" Instruções de DNS:"));
|
|
2703
|
+
console.log(chalk.white(` ${result.dnsInstruction}`));
|
|
2704
|
+
console.log();
|
|
2705
|
+
info(`Após configurar o DNS, execute: ${chalk.bold(`veloz domains verify ${result.id.slice(0, 8)}`)}`);
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
spin.stop();
|
|
2708
|
+
handleError(error);
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
domainsCommand.command("verify <domainId>").alias("verificar").description("Verificar configuração DNS de um domínio").action(async (domainId) => {
|
|
2712
|
+
const spin = spinner("Verificando DNS...");
|
|
2713
|
+
try {
|
|
2714
|
+
const result = await (await getClient()).domains.verify({ domainId });
|
|
2715
|
+
spin.stop();
|
|
2716
|
+
if (result.verified) {
|
|
2717
|
+
success(`Domínio ${chalk.bold(result.domain.domain)} verificado com sucesso!`);
|
|
2718
|
+
info("O certificado TLS será provisionado automaticamente.");
|
|
2719
|
+
} else {
|
|
2720
|
+
console.log(chalk.yellow(`\n⚠ Domínio ${chalk.bold(result.domain.domain)} ainda não verificado.`));
|
|
2721
|
+
info("Verifique se o CNAME foi propagado e tente novamente.");
|
|
2722
|
+
}
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
spin.stop();
|
|
2725
|
+
handleError(error);
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
domainsCommand.command("delete <domainId>").alias("deletar").description("Remover domínio").action(async (domainId) => {
|
|
2729
|
+
const spin = spinner("Removendo domínio...");
|
|
2730
|
+
try {
|
|
2731
|
+
await (await getClient()).domains.delete({ domainId });
|
|
2732
|
+
spin.stop();
|
|
2733
|
+
success("Domínio removido com sucesso.");
|
|
2734
|
+
} catch (error) {
|
|
2735
|
+
spin.stop();
|
|
2736
|
+
handleError(error);
|
|
2737
|
+
}
|
|
2738
|
+
});
|
|
2739
|
+
|
|
2740
|
+
//#endregion
|
|
2741
|
+
//#region src/commands/config.ts
|
|
2742
|
+
function formatValue(value) {
|
|
2743
|
+
if (value === null || value === void 0 || value === "") return chalk.dim("—");
|
|
2744
|
+
return chalk.cyan(String(value));
|
|
2745
|
+
}
|
|
2746
|
+
function printServiceConfig(service) {
|
|
2747
|
+
console.log(` ${chalk.bold("Nome:")} ${formatValue(service.name)}`);
|
|
2748
|
+
console.log(` ${chalk.bold("Tipo:")} ${formatValue(service.type === "WEB" ? "Serviço Web" : "Site Estático")}`);
|
|
2749
|
+
console.log(` ${chalk.bold("Branch:")} ${formatValue(service.branch)}`);
|
|
2750
|
+
console.log(` ${chalk.bold("Root Dir:")} ${formatValue(service.rootDirectory || "/")}`);
|
|
2751
|
+
console.log(` ${chalk.bold("Build Command:")} ${formatValue(service.buildCommand)}`);
|
|
2752
|
+
console.log(` ${chalk.bold("Start Command:")} ${formatValue(service.startCommand)}`);
|
|
2753
|
+
console.log(` ${chalk.bold("Porta:")} ${formatValue(service.port)}`);
|
|
2754
|
+
console.log(` ${chalk.bold("Instâncias:")} ${formatValue(service.instanceCount)}`);
|
|
2755
|
+
console.log(` ${chalk.bold("CPU Limit:")} ${formatValue(service.cpuLimit)}`);
|
|
2756
|
+
console.log(` ${chalk.bold("Memory Limit:")} ${formatValue(service.memoryLimit)}`);
|
|
2757
|
+
}
|
|
2758
|
+
const configCommand = new Command("config").description("Gerenciar configurações do serviço");
|
|
2759
|
+
configCommand.command("show").description("Mostrar configurações atuais dos serviços").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const { services } = resolveAllServices(opts.service);
|
|
2762
|
+
const client = await getClient();
|
|
2763
|
+
const showHeaders = services.length > 1;
|
|
2764
|
+
const spin = spinner("Buscando configurações...");
|
|
2765
|
+
for (const { service: svcConfig, index } of services) {
|
|
2766
|
+
const service = await client.services.get({ serviceId: svcConfig.id });
|
|
2767
|
+
if (showHeaders) console.log(`\n${getServiceHeader(svcConfig.name, index)}\n`);
|
|
2768
|
+
else console.log(chalk.bold("\n📋 Configurações do Serviço\n"));
|
|
2769
|
+
printServiceConfig(service);
|
|
2770
|
+
console.log();
|
|
2771
|
+
}
|
|
2772
|
+
spin.stop();
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
handleError(error);
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
configCommand.command("set").description("Atualizar configurações do serviço").option("-n, --name <name>", "Nome do serviço").option("-b, --build <command>", "Comando de build").option("-s, --start <command>", "Comando de start").option("-p, --port <port>", "Porta do serviço").option("-r, --root <dir>", "Diretório raiz").option("-i, --instances <count>", "Número de instâncias").option("--cpu <limit>", "Limite de CPU (ex: 500m, 1)").option("--memory <limit>", "Limite de memória (ex: 512Mi, 1Gi)").option("--branch <branch>", "Branch do Git").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (options) => {
|
|
2778
|
+
try {
|
|
2779
|
+
const serviceId = await resolveServiceId(options.service);
|
|
2780
|
+
const client = await getClient();
|
|
2781
|
+
const updates = {};
|
|
2782
|
+
if (options.name) updates.name = options.name;
|
|
2783
|
+
if (options.build !== void 0) updates.buildCommand = options.build === "none" ? null : options.build;
|
|
2784
|
+
if (options.start !== void 0) updates.startCommand = options.start === "none" ? null : options.start;
|
|
2785
|
+
if (options.port) updates.port = parseInt(options.port, 10);
|
|
2786
|
+
if (options.root !== void 0) updates.rootDirectory = options.root === "/" ? null : options.root;
|
|
2787
|
+
if (options.instances) updates.instanceCount = parseInt(options.instances, 10);
|
|
2788
|
+
if (options.cpu) updates.cpuLimit = options.cpu;
|
|
2789
|
+
if (options.memory) updates.memoryLimit = options.memory;
|
|
2790
|
+
if (options.branch) updates.branch = options.branch;
|
|
2791
|
+
if (Object.keys(updates).length === 0) {
|
|
2792
|
+
console.error(chalk.yellow("⚠ Nenhuma configuração fornecida para atualizar."));
|
|
2793
|
+
console.log(chalk.dim("\nUse 'veloz config set --help' para ver as opções disponíveis."));
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const spin = spinner("Atualizando configurações...");
|
|
2797
|
+
await client.services.update({
|
|
2798
|
+
serviceId,
|
|
2799
|
+
...updates
|
|
2800
|
+
});
|
|
2801
|
+
spin.stop();
|
|
2802
|
+
success("Configurações atualizadas com sucesso!");
|
|
2803
|
+
console.log(chalk.dim("\nValores atualizados:"));
|
|
2804
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
2805
|
+
const displayKey = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
|
|
2806
|
+
console.log(` ${chalk.bold(displayKey)}: ${formatValue(value)}`);
|
|
2807
|
+
}
|
|
2808
|
+
console.log();
|
|
2809
|
+
info("Execute 'veloz deploy' para aplicar as mudanças.");
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
handleError(error);
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
configCommand.command("edit").description("Editar configurações interativamente").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (opts) => {
|
|
2815
|
+
try {
|
|
2816
|
+
const serviceId = await resolveServiceId(opts.service);
|
|
2817
|
+
const client = await getClient();
|
|
2818
|
+
const spin = spinner("Buscando configurações atuais...");
|
|
2819
|
+
const service = await client.services.get({ serviceId });
|
|
2820
|
+
spin.stop();
|
|
2821
|
+
console.log(chalk.bold("\n📝 Editar Configurações\n"));
|
|
2822
|
+
console.log(chalk.dim("Pressione Enter para manter o valor atual, digite 'none' para remover\n"));
|
|
2823
|
+
const updates = {};
|
|
2824
|
+
const name = await prompt(`Nome do serviço ${chalk.dim(`(${service.name})`)}: `);
|
|
2825
|
+
if (name) updates.name = name;
|
|
2826
|
+
const buildCmd = await prompt(`Build command ${chalk.dim(`(${service.buildCommand || "—"})`)}: `);
|
|
2827
|
+
if (buildCmd) updates.buildCommand = buildCmd === "none" ? null : buildCmd;
|
|
2828
|
+
const startCmd$1 = await prompt(`Start command ${chalk.dim(`(${service.startCommand || "—"})`)}: `);
|
|
2829
|
+
if (startCmd$1) updates.startCommand = startCmd$1 === "none" ? null : startCmd$1;
|
|
2830
|
+
const port = await prompt(`Porta ${chalk.dim(`(${service.port})`)}: `);
|
|
2831
|
+
if (port) updates.port = parseInt(port, 10);
|
|
2832
|
+
const rootDir = await prompt(`Diretório raiz ${chalk.dim(`(${service.rootDirectory || "/"})`)}: `);
|
|
2833
|
+
if (rootDir) updates.rootDirectory = rootDir === "/" ? null : rootDir;
|
|
2834
|
+
const instances = await prompt(`Número de instâncias ${chalk.dim(`(${service.instanceCount})`)}: `);
|
|
2835
|
+
if (instances) updates.instanceCount = parseInt(instances, 10);
|
|
2836
|
+
const cpu = await prompt(`Limite de CPU ${chalk.dim(`(${service.cpuLimit})`)}: `);
|
|
2837
|
+
if (cpu) updates.cpuLimit = cpu;
|
|
2838
|
+
const memory = await prompt(`Limite de memória ${chalk.dim(`(${service.memoryLimit})`)}: `);
|
|
2839
|
+
if (memory) updates.memoryLimit = memory;
|
|
2840
|
+
if (Object.keys(updates).length === 0) {
|
|
2841
|
+
info("Nenhuma alteração realizada.");
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
const updateSpin = spinner("Salvando configurações...");
|
|
2845
|
+
await client.services.update({
|
|
2846
|
+
serviceId,
|
|
2847
|
+
...updates
|
|
2848
|
+
});
|
|
2849
|
+
updateSpin.stop();
|
|
2850
|
+
success("Configurações atualizadas com sucesso!");
|
|
2851
|
+
info("Execute 'veloz deploy' para aplicar as mudanças.");
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
handleError(error);
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
configCommand.command("reset").description("Resetar configurações para os padrões").option("--build", "Resetar comando de build").option("--start", "Resetar comando de start").option("--all", "Resetar todas as configurações opcionais").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (options) => {
|
|
2857
|
+
try {
|
|
2858
|
+
const serviceId = await resolveServiceId(options.service);
|
|
2859
|
+
const client = await getClient();
|
|
2860
|
+
const updates = {};
|
|
2861
|
+
if (options.all) {
|
|
2862
|
+
updates.buildCommand = null;
|
|
2863
|
+
updates.startCommand = null;
|
|
2864
|
+
updates.rootDirectory = null;
|
|
2865
|
+
} else {
|
|
2866
|
+
if (options.build) updates.buildCommand = null;
|
|
2867
|
+
if (options.start) updates.startCommand = null;
|
|
2868
|
+
}
|
|
2869
|
+
if (Object.keys(updates).length === 0) {
|
|
2870
|
+
console.error(chalk.yellow("⚠ Especifique o que resetar: --build, --start, ou --all"));
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
const spin = spinner("Resetando configurações...");
|
|
2874
|
+
await client.services.update({
|
|
2875
|
+
serviceId,
|
|
2876
|
+
...updates
|
|
2877
|
+
});
|
|
2878
|
+
spin.stop();
|
|
2879
|
+
success("Configurações resetadas para os padrões!");
|
|
2880
|
+
info("Execute 'veloz deploy' para aplicar as mudanças.");
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
handleError(error);
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region src/commands/use.ts
|
|
2888
|
+
const useCommand = new Command("use").description("Selecionar qual serviço usar como padrão").argument("[serviço]", "Nome ou chave do serviço").action(async (servicoArg) => {
|
|
2889
|
+
try {
|
|
2890
|
+
const config = loadConfig();
|
|
2891
|
+
if (!config) {
|
|
2892
|
+
console.error(chalk.red("\n✗ Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
|
|
2893
|
+
process.exit(1);
|
|
2894
|
+
}
|
|
2895
|
+
const services = Object.entries(config.services);
|
|
2896
|
+
if (services.length === 0) {
|
|
2897
|
+
console.error(chalk.red("\n✗ Nenhum serviço encontrado na configuração."));
|
|
2898
|
+
process.exit(1);
|
|
2899
|
+
}
|
|
2900
|
+
if (services.length === 1) {
|
|
2901
|
+
info("Apenas um serviço configurado — já é o padrão.");
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
const currentDefault = getDefaultServiceKey();
|
|
2905
|
+
let selectedKey;
|
|
2906
|
+
if (servicoArg) {
|
|
2907
|
+
const found = services.find(([key, service]) => key === servicoArg || service.name.toLowerCase() === servicoArg.toLowerCase());
|
|
2908
|
+
if (!found) {
|
|
2909
|
+
console.error(chalk.red(`\n✗ Serviço '${servicoArg}' não encontrado.`));
|
|
2910
|
+
console.log(chalk.dim("\nServiços disponíveis:"));
|
|
2911
|
+
services.forEach(([key, service], i) => {
|
|
2912
|
+
const marker = key === currentDefault ? chalk.cyan(" ← padrão") : "";
|
|
2913
|
+
console.log(` ${getServiceHeader(service.name, i)} ${chalk.dim(key)}${marker}`);
|
|
2914
|
+
});
|
|
2915
|
+
process.exit(1);
|
|
2916
|
+
}
|
|
2917
|
+
selectedKey = found[0];
|
|
2918
|
+
} else selectedKey = await promptSelect("Qual serviço usar como padrão?", services.map(([key, service]) => ({
|
|
2919
|
+
label: `${service.name} (${key})${key === currentDefault ? chalk.cyan(" ← atual") : ""}`,
|
|
2920
|
+
value: key
|
|
2921
|
+
})));
|
|
2922
|
+
setDefaultServiceKey(selectedKey);
|
|
2923
|
+
const selectedService = config.services[selectedKey];
|
|
2924
|
+
success(`Serviço padrão: ${chalk.bold(selectedService.name)} (${selectedKey})`);
|
|
2925
|
+
console.log(chalk.dim(`\nComandos como ${chalk.white("logs")}, ${chalk.white("env")}, ${chalk.white("config")} vão usar este serviço automaticamente.`));
|
|
2926
|
+
console.log(chalk.dim(`Use ${chalk.white("--service <nome>")} para sobrescrever pontualmente.`));
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
handleError(error);
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
//#endregion
|
|
2933
|
+
//#region src/index.ts
|
|
2934
|
+
const version = process.env.npm_package_version;
|
|
2935
|
+
const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version(version ?? "0.0.0-beta.1");
|
|
2936
|
+
program.addCommand(loginCommand);
|
|
2937
|
+
program.addCommand(logoutCommand);
|
|
2938
|
+
program.addCommand(projectsCommand);
|
|
2939
|
+
program.addCommand(linkCommand);
|
|
2940
|
+
program.addCommand(deployCommand);
|
|
2941
|
+
program.addCommand(logsCommand);
|
|
2942
|
+
program.addCommand(envCommand);
|
|
2943
|
+
program.addCommand(domainsCommand);
|
|
2944
|
+
program.addCommand(configCommand);
|
|
2945
|
+
program.addCommand(useCommand);
|
|
2946
|
+
program.parse();
|
|
2947
|
+
|
|
2948
|
+
//#endregion
|
|
2949
|
+
export { };
|