onveloz 0.0.0-beta.1 → 0.0.0-beta.10
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 +2386 -1447
- package/package.json +16 -14
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "@commander-js/extra-typings";
|
|
3
|
-
import
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { exec, execSync } from "node:child_process";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import chalk from "chalk";
|
|
5
8
|
import { homedir, platform, tmpdir } from "node:os";
|
|
6
9
|
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
10
|
import * as readline from "node:readline";
|
|
10
11
|
import { createInterface } from "node:readline";
|
|
11
12
|
import ora from "ora";
|
|
@@ -13,11 +14,315 @@ import { createAuthClient } from "better-auth/client";
|
|
|
13
14
|
import { deviceAuthorizationClient } from "better-auth/client/plugins";
|
|
14
15
|
import { createORPCClient } from "@orpc/client";
|
|
15
16
|
import { RPCLink } from "@orpc/client/fetch";
|
|
16
|
-
import {
|
|
17
|
-
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
17
|
+
import { link, mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
18
18
|
import ignore from "ignore";
|
|
19
19
|
import tar from "tar";
|
|
20
20
|
|
|
21
|
+
//#region src/lib/output.ts
|
|
22
|
+
let _outputMode = null;
|
|
23
|
+
function getOutputMode() {
|
|
24
|
+
if (_outputMode) return _outputMode;
|
|
25
|
+
return detectOutputMode();
|
|
26
|
+
}
|
|
27
|
+
function detectOutputMode() {
|
|
28
|
+
if (process.env.VELOZ_OUTPUT === "json") return "json";
|
|
29
|
+
if (process.env.VELOZ_OUTPUT === "github-actions") return "github-actions";
|
|
30
|
+
if (process.env.VELOZ_OUTPUT === "plain") return "plain";
|
|
31
|
+
if (process.env.GITHUB_ACTIONS === "true") return "github-actions";
|
|
32
|
+
if (process.env.CI === "true") return "plain";
|
|
33
|
+
if (!process.stdout.isTTY || process.env.TERM === "dumb") return "plain";
|
|
34
|
+
return "fancy";
|
|
35
|
+
}
|
|
36
|
+
function setOutputMode(mode) {
|
|
37
|
+
_outputMode = mode;
|
|
38
|
+
}
|
|
39
|
+
function isInteractive() {
|
|
40
|
+
return getOutputMode() === "fancy" && process.stdin.isTTY === true;
|
|
41
|
+
}
|
|
42
|
+
function isMachineReadable() {
|
|
43
|
+
return getOutputMode() === "json";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Emit structured JSON data to stdout.
|
|
47
|
+
* In JSON mode: outputs raw JSON object.
|
|
48
|
+
* In other modes: no-op (callers should use regular output helpers).
|
|
49
|
+
*/
|
|
50
|
+
function emitData(data) {
|
|
51
|
+
if (isMachineReadable()) process.stdout.write(JSON.stringify(data) + "\n");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Open a collapsible group (GitHub Actions) or section header.
|
|
55
|
+
*/
|
|
56
|
+
function startGroup(name) {
|
|
57
|
+
if (getOutputMode() === "github-actions") process.stdout.write(`::group::${name}\n`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Close a collapsible group (GitHub Actions).
|
|
61
|
+
*/
|
|
62
|
+
function endGroup() {
|
|
63
|
+
if (getOutputMode() === "github-actions") process.stdout.write("::endgroup::\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region ../../packages/config/veloz-config.ts
|
|
68
|
+
const ServiceTypeSchema = z.enum(["web", "static"]);
|
|
69
|
+
const PackageManagerSchema = z.enum([
|
|
70
|
+
"npm",
|
|
71
|
+
"yarn",
|
|
72
|
+
"pnpm",
|
|
73
|
+
"bun",
|
|
74
|
+
"auto"
|
|
75
|
+
]);
|
|
76
|
+
const BuildConfigSchema = z.object({
|
|
77
|
+
command: z.string().nullable().optional(),
|
|
78
|
+
nodeVersion: z.string().regex(/^[0-9]+(\.[0-9]+){0,2}(\.x)?$/).default("20").optional(),
|
|
79
|
+
nixpkgsArchive: z.string().regex(/^[a-f0-9]{40}$/).optional(),
|
|
80
|
+
packageManager: PackageManagerSchema.default("auto").optional(),
|
|
81
|
+
installCommand: z.string().nullable().optional(),
|
|
82
|
+
outputDir: z.string().nullable().optional(),
|
|
83
|
+
aptPackages: z.array(z.string().regex(/^[a-z0-9][a-z0-9.+\-]+$/, "Nome de pacote inválido")).optional()
|
|
84
|
+
});
|
|
85
|
+
const RuntimeConfigSchema = z.object({
|
|
86
|
+
command: z.string().nullable().optional(),
|
|
87
|
+
port: z.number().min(1).max(65535).default(3e3).optional(),
|
|
88
|
+
healthCheck: z.object({
|
|
89
|
+
path: z.string().default("/").optional(),
|
|
90
|
+
interval: z.number().default(30).optional(),
|
|
91
|
+
timeout: z.number().default(10).optional()
|
|
92
|
+
}).optional()
|
|
93
|
+
});
|
|
94
|
+
const ResourcesSchema = z.object({
|
|
95
|
+
instances: z.number().min(1).max(10).default(1).optional(),
|
|
96
|
+
cpu: z.string().regex(/^[0-9]+(\.[0-9]+)?|[0-9]+m$/).default("500m").optional(),
|
|
97
|
+
memory: z.string().regex(/^[0-9]+(Mi|Gi)$/).default("512Mi").optional(),
|
|
98
|
+
autoscale: z.object({
|
|
99
|
+
enabled: z.boolean().default(false).optional(),
|
|
100
|
+
minInstances: z.number().min(1).default(1).optional(),
|
|
101
|
+
maxInstances: z.number().min(1).max(20).default(3).optional(),
|
|
102
|
+
targetCPU: z.number().min(10).max(90).default(70).optional()
|
|
103
|
+
}).optional()
|
|
104
|
+
});
|
|
105
|
+
const EnvVarDefinitionSchema = z.object({
|
|
106
|
+
description: z.string().optional(),
|
|
107
|
+
required: z.boolean().default(false).optional(),
|
|
108
|
+
example: z.string().optional()
|
|
109
|
+
});
|
|
110
|
+
const ServiceConfigSchema = z.object({
|
|
111
|
+
id: z.string(),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
type: ServiceTypeSchema.default("web"),
|
|
114
|
+
root: z.string().default("."),
|
|
115
|
+
branch: z.string().optional(),
|
|
116
|
+
build: BuildConfigSchema.optional(),
|
|
117
|
+
runtime: RuntimeConfigSchema.optional(),
|
|
118
|
+
env: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
|
|
119
|
+
resources: ResourcesSchema.optional()
|
|
120
|
+
});
|
|
121
|
+
const ProjectConfigSchema = z.object({
|
|
122
|
+
id: z.string(),
|
|
123
|
+
name: z.string(),
|
|
124
|
+
slug: z.string().regex(/^[a-z0-9-]+$/).optional()
|
|
125
|
+
});
|
|
126
|
+
const ServiceDefaultsSchema = z.object({
|
|
127
|
+
type: ServiceTypeSchema.optional(),
|
|
128
|
+
branch: z.string().optional(),
|
|
129
|
+
build: BuildConfigSchema.optional(),
|
|
130
|
+
runtime: RuntimeConfigSchema.optional(),
|
|
131
|
+
resources: ResourcesSchema.optional()
|
|
132
|
+
});
|
|
133
|
+
const EnvironmentServiceOverrideSchema = z.object({
|
|
134
|
+
id: z.string(),
|
|
135
|
+
name: z.string().optional()
|
|
136
|
+
});
|
|
137
|
+
const EnvironmentOverrideSchema = z.object({
|
|
138
|
+
project: ProjectConfigSchema,
|
|
139
|
+
services: z.record(z.string(), EnvironmentServiceOverrideSchema)
|
|
140
|
+
});
|
|
141
|
+
const VelozConfigSchema = z.object({
|
|
142
|
+
$schema: z.string().optional(),
|
|
143
|
+
version: z.literal("1.0"),
|
|
144
|
+
project: ProjectConfigSchema,
|
|
145
|
+
services: z.record(z.string(), ServiceConfigSchema),
|
|
146
|
+
defaults: ServiceDefaultsSchema.optional(),
|
|
147
|
+
environments: z.record(z.string(), EnvironmentOverrideSchema).optional(),
|
|
148
|
+
created: z.string().datetime().optional(),
|
|
149
|
+
updated: z.string().datetime().optional()
|
|
150
|
+
});
|
|
151
|
+
function mergeServiceWithDefaults(service, defaults) {
|
|
152
|
+
if (!defaults) return service;
|
|
153
|
+
return {
|
|
154
|
+
...service,
|
|
155
|
+
type: service.type ?? defaults.type ?? "web",
|
|
156
|
+
branch: service.branch ?? defaults.branch,
|
|
157
|
+
build: {
|
|
158
|
+
...defaults.build,
|
|
159
|
+
...service.build
|
|
160
|
+
},
|
|
161
|
+
runtime: {
|
|
162
|
+
...defaults.runtime,
|
|
163
|
+
...service.runtime
|
|
164
|
+
},
|
|
165
|
+
resources: {
|
|
166
|
+
...defaults.resources,
|
|
167
|
+
...service.resources
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function parseVelozConfig(data) {
|
|
172
|
+
return VelozConfigSchema.parse(data);
|
|
173
|
+
}
|
|
174
|
+
function resolveConfigForEnv(config, env) {
|
|
175
|
+
const envOverride = config.environments?.[env];
|
|
176
|
+
if (!envOverride) return null;
|
|
177
|
+
const resolvedServices = {};
|
|
178
|
+
for (const [key, service] of Object.entries(config.services)) {
|
|
179
|
+
const override = envOverride.services[key];
|
|
180
|
+
if (override) resolvedServices[key] = {
|
|
181
|
+
...service,
|
|
182
|
+
id: override.id,
|
|
183
|
+
name: override.name ?? service.name
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
...config,
|
|
188
|
+
project: envOverride.project,
|
|
189
|
+
services: resolvedServices,
|
|
190
|
+
environments: void 0
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/lib/link.ts
|
|
196
|
+
const CONFIG_FILE$1 = "veloz.json";
|
|
197
|
+
const LOCAL_CONFIG_FILE = "veloz.local.json";
|
|
198
|
+
let _activeEnv;
|
|
199
|
+
function setActiveEnv(env) {
|
|
200
|
+
_activeEnv = env;
|
|
201
|
+
}
|
|
202
|
+
function getActiveEnv() {
|
|
203
|
+
return _activeEnv ?? process.env.VELOZ_ENV;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Find the root directory (git root or monorepo root)
|
|
207
|
+
*/
|
|
208
|
+
function findProjectRoot(startPath = process.cwd()) {
|
|
209
|
+
let currentPath = resolve(startPath);
|
|
210
|
+
const root = resolve("/");
|
|
211
|
+
while (currentPath !== root) {
|
|
212
|
+
if (existsSync(join(currentPath, ".git"))) return currentPath;
|
|
213
|
+
if (existsSync(join(currentPath, "pnpm-workspace.yaml")) || existsSync(join(currentPath, "lerna.json")) || existsSync(join(currentPath, "rush.json")) || existsSync(join(currentPath, "nx.json"))) return currentPath;
|
|
214
|
+
const pkgJsonPath = join(currentPath, "package.json");
|
|
215
|
+
if (existsSync(pkgJsonPath)) try {
|
|
216
|
+
if (JSON.parse(readFileSync(pkgJsonPath, "utf-8")).workspaces) return currentPath;
|
|
217
|
+
} catch {}
|
|
218
|
+
const parentPath = resolve(currentPath, "..");
|
|
219
|
+
if (parentPath === currentPath) break;
|
|
220
|
+
currentPath = parentPath;
|
|
221
|
+
}
|
|
222
|
+
return process.cwd();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Get the config file name (veloz.local.json when VELOZ_API_URL is set, veloz.json otherwise)
|
|
226
|
+
*/
|
|
227
|
+
function getConfigFileName() {
|
|
228
|
+
return process.env.VELOZ_API_URL ? LOCAL_CONFIG_FILE : CONFIG_FILE$1;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get the path to veloz.json (or veloz.local.json when using custom API URL)
|
|
232
|
+
*/
|
|
233
|
+
function getConfigPath() {
|
|
234
|
+
return join(findProjectRoot(), getConfigFileName());
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Load the veloz.json config from project root (without environment resolution)
|
|
238
|
+
*/
|
|
239
|
+
function loadRawConfig() {
|
|
240
|
+
const path = getConfigPath();
|
|
241
|
+
if (!existsSync(path)) return null;
|
|
242
|
+
try {
|
|
243
|
+
const raw = readFileSync(path, "utf-8");
|
|
244
|
+
return parseVelozConfig(JSON.parse(raw));
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error(`Error loading ${getConfigFileName()}:`, error);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Load the veloz.json config, resolving environment overrides if --env is active.
|
|
252
|
+
* Returns the env-resolved config when an env is active and configured,
|
|
253
|
+
* or the base config when no env is active.
|
|
254
|
+
* Returns null if the config file doesn't exist or the active env isn't configured.
|
|
255
|
+
*/
|
|
256
|
+
function loadConfig() {
|
|
257
|
+
const config = loadRawConfig();
|
|
258
|
+
if (!config) return null;
|
|
259
|
+
const env = getActiveEnv();
|
|
260
|
+
if (env) return resolveConfigForEnv(config, env);
|
|
261
|
+
return config;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Save the veloz.json config to project root
|
|
265
|
+
*/
|
|
266
|
+
function saveConfig(config) {
|
|
267
|
+
const path = getConfigPath();
|
|
268
|
+
const configWithSchema = {
|
|
269
|
+
$schema: "https://onveloz.com/schemas/veloz-config.schema.json",
|
|
270
|
+
...config
|
|
271
|
+
};
|
|
272
|
+
writeFileSync(path, JSON.stringify(configWithSchema, null, 2), "utf-8");
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Require config to exist, throw if not found
|
|
276
|
+
*/
|
|
277
|
+
function requireConfig() {
|
|
278
|
+
const config = loadConfig();
|
|
279
|
+
if (!config) throw new Error(`No ${getConfigFileName()} found in project root. Run 'veloz init' or 'veloz deploy' to set up your project.`);
|
|
280
|
+
return config;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Check if current directory is a git repository
|
|
284
|
+
*/
|
|
285
|
+
function isGitRepo() {
|
|
286
|
+
try {
|
|
287
|
+
execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
288
|
+
return true;
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get git remote origin info
|
|
295
|
+
*/
|
|
296
|
+
function getGitRemote() {
|
|
297
|
+
try {
|
|
298
|
+
const url = execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
|
|
299
|
+
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
300
|
+
if (httpsMatch) return {
|
|
301
|
+
owner: httpsMatch[1],
|
|
302
|
+
repo: httpsMatch[2]
|
|
303
|
+
};
|
|
304
|
+
const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/);
|
|
305
|
+
if (sshMatch) return {
|
|
306
|
+
owner: sshMatch[1],
|
|
307
|
+
repo: sshMatch[2]
|
|
308
|
+
};
|
|
309
|
+
return null;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get current git branch
|
|
316
|
+
*/
|
|
317
|
+
function getGitBranch() {
|
|
318
|
+
try {
|
|
319
|
+
return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
|
|
320
|
+
} catch {
|
|
321
|
+
return "main";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//#endregion
|
|
21
326
|
//#region src/lib/config.ts
|
|
22
327
|
const CONFIG_DIR = join(homedir(), ".veloz");
|
|
23
328
|
const ENV_API_URL = process.env.VELOZ_API_URL;
|
|
@@ -26,21 +331,22 @@ function getConfigFile() {
|
|
|
26
331
|
if (!ENV_API_URL) return join(CONFIG_DIR, "config.json");
|
|
27
332
|
return join(CONFIG_DIR, `config.${createHash("md5").update(ENV_API_URL).digest("hex").slice(0, 8)}.json`);
|
|
28
333
|
}
|
|
29
|
-
const CONFIG_FILE
|
|
334
|
+
const CONFIG_FILE = getConfigFile();
|
|
30
335
|
function ensureConfigDir() {
|
|
31
336
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
337
|
}
|
|
33
338
|
function loadConfig$1() {
|
|
34
|
-
if (!existsSync(CONFIG_FILE
|
|
339
|
+
if (!existsSync(CONFIG_FILE)) return {
|
|
35
340
|
apiKey: "",
|
|
36
341
|
apiUrl: DEFAULT_API_URL
|
|
37
342
|
};
|
|
38
343
|
try {
|
|
39
|
-
const raw = readFileSync(CONFIG_FILE
|
|
344
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
40
345
|
const parsed = JSON.parse(raw);
|
|
41
346
|
return {
|
|
42
347
|
apiKey: parsed.apiKey ?? "",
|
|
43
|
-
apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL
|
|
348
|
+
apiUrl: ENV_API_URL ?? parsed.apiUrl ?? DEFAULT_API_URL,
|
|
349
|
+
organizationId: parsed.organizationId
|
|
44
350
|
};
|
|
45
351
|
} catch {
|
|
46
352
|
return {
|
|
@@ -54,20 +360,28 @@ function saveConfig$1(config) {
|
|
|
54
360
|
const existing = loadConfig$1();
|
|
55
361
|
const merged = {
|
|
56
362
|
apiKey: config.apiKey ?? existing.apiKey,
|
|
57
|
-
apiUrl: config.apiUrl ?? existing.apiUrl
|
|
363
|
+
apiUrl: config.apiUrl ?? existing.apiUrl,
|
|
364
|
+
organizationId: config.organizationId ?? existing.organizationId
|
|
58
365
|
};
|
|
59
|
-
writeFileSync(CONFIG_FILE
|
|
366
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
|
|
60
367
|
}
|
|
61
368
|
function deleteConfig() {
|
|
62
|
-
if (existsSync(CONFIG_FILE
|
|
369
|
+
if (existsSync(CONFIG_FILE)) unlinkSync(CONFIG_FILE);
|
|
63
370
|
}
|
|
64
371
|
function isAuthenticated() {
|
|
65
372
|
return loadConfig$1().apiKey.length > 0;
|
|
66
373
|
}
|
|
67
374
|
let envApiUrlLogged = false;
|
|
68
|
-
async function requireAuth() {
|
|
375
|
+
async function requireAuth(options) {
|
|
376
|
+
const envApiKey = process.env.VELOZ_API_KEY;
|
|
377
|
+
if (envApiKey) return {
|
|
378
|
+
apiKey: envApiKey,
|
|
379
|
+
apiUrl: ENV_API_URL ?? DEFAULT_API_URL
|
|
380
|
+
};
|
|
381
|
+
if ("VELOZ_API_KEY" in process.env) throw new Error("VELOZ_API_KEY está definida mas vazia. Verifique o valor do secret.");
|
|
69
382
|
let config = loadConfig$1();
|
|
70
383
|
if (!config.apiKey) {
|
|
384
|
+
if (options?.nonInteractive) throw new Error("Nenhuma autenticação encontrada. Defina VELOZ_API_KEY ou execute `veloz login`.");
|
|
71
385
|
await performLogin(config.apiUrl);
|
|
72
386
|
config = loadConfig$1();
|
|
73
387
|
}
|
|
@@ -87,10 +401,28 @@ function isRateLimitError(error) {
|
|
|
87
401
|
return null;
|
|
88
402
|
}
|
|
89
403
|
function handleError(error) {
|
|
404
|
+
const mode = getOutputMode();
|
|
90
405
|
if (error instanceof Error) {
|
|
91
406
|
const orpcError = error;
|
|
92
407
|
if (orpcError.code) {
|
|
93
|
-
const
|
|
408
|
+
const orpcData = orpcError.data;
|
|
409
|
+
if (orpcData?.code === "ACCESS_NOT_APPROVED") {
|
|
410
|
+
const gateMsg = "Sua conta ainda não foi aprovada para usar a Veloz.";
|
|
411
|
+
const gateHint = "Acesse app.onveloz.com para solicitar acesso.";
|
|
412
|
+
if (mode === "json") emitData({
|
|
413
|
+
type: "error",
|
|
414
|
+
code: "ACCESS_NOT_APPROVED",
|
|
415
|
+
message: gateMsg,
|
|
416
|
+
hint: gateHint
|
|
417
|
+
});
|
|
418
|
+
else if (mode === "github-actions") process.stdout.write(`::error::${gateMsg} ${gateHint}\n`);
|
|
419
|
+
else {
|
|
420
|
+
console.error(chalk.red(`\n✗ ${gateMsg}`));
|
|
421
|
+
console.error(chalk.yellow(` ${gateHint}`));
|
|
422
|
+
}
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const message = orpcData?.message ?? orpcError.message ?? {
|
|
94
426
|
NOT_FOUND: "Recurso não encontrado.",
|
|
95
427
|
FORBIDDEN: "Sem permissão para acessar este recurso.",
|
|
96
428
|
CONFLICT: "Conflito — recurso já existe.",
|
|
@@ -99,51 +431,189 @@ function handleError(error) {
|
|
|
99
431
|
INTERNAL_SERVER_ERROR: "Erro interno do servidor. Tente novamente.",
|
|
100
432
|
TOO_MANY_REQUESTS: "Tente novamente em alguns segundos."
|
|
101
433
|
}[orpcError.code] ?? "Erro desconhecido.";
|
|
102
|
-
|
|
434
|
+
if (mode === "json") emitData({
|
|
435
|
+
type: "error",
|
|
436
|
+
code: orpcError.code,
|
|
437
|
+
message
|
|
438
|
+
});
|
|
439
|
+
else if (mode === "github-actions") process.stdout.write(`::error::${message}\n`);
|
|
440
|
+
else console.error(chalk.red(`\n✗ Erro: ${message}`));
|
|
103
441
|
process.exit(1);
|
|
104
442
|
}
|
|
105
443
|
if (error.message.includes("fetch") || error.message.includes("ECONNREFUSED")) {
|
|
106
|
-
|
|
107
|
-
|
|
444
|
+
const message = "Não foi possível conectar ao servidor Veloz.";
|
|
445
|
+
const hint = "Verifique se o servidor está rodando e a URL está correta.";
|
|
446
|
+
if (mode === "json") emitData({
|
|
447
|
+
type: "error",
|
|
448
|
+
code: "CONNECTION_ERROR",
|
|
449
|
+
message,
|
|
450
|
+
hint
|
|
451
|
+
});
|
|
452
|
+
else if (mode === "github-actions") process.stdout.write(`::error::${message} ${hint}\n`);
|
|
453
|
+
else {
|
|
454
|
+
console.error(chalk.red(`\n✗ Erro de conexão: ${message}`));
|
|
455
|
+
console.error(chalk.yellow(` ${hint}`));
|
|
456
|
+
}
|
|
108
457
|
process.exit(1);
|
|
109
458
|
}
|
|
110
|
-
|
|
459
|
+
if (mode === "json") emitData({
|
|
460
|
+
type: "error",
|
|
461
|
+
message: error.message
|
|
462
|
+
});
|
|
463
|
+
else if (mode === "github-actions") process.stdout.write(`::error::${error.message}\n`);
|
|
464
|
+
else console.error(chalk.red(`\n✗ Erro: ${error.message}`));
|
|
111
465
|
process.exit(1);
|
|
112
466
|
}
|
|
113
|
-
|
|
467
|
+
if (mode === "json") emitData({
|
|
468
|
+
type: "error",
|
|
469
|
+
message: "Erro inesperado."
|
|
470
|
+
});
|
|
471
|
+
else if (mode === "github-actions") process.stdout.write("::error::Erro inesperado.\n");
|
|
472
|
+
else console.error(chalk.red("\n✗ Erro inesperado."));
|
|
114
473
|
process.exit(1);
|
|
115
474
|
}
|
|
116
475
|
function success(message) {
|
|
117
|
-
|
|
476
|
+
const mode = getOutputMode();
|
|
477
|
+
if (mode === "json") emitData({
|
|
478
|
+
type: "success",
|
|
479
|
+
message
|
|
480
|
+
});
|
|
481
|
+
else if (mode === "github-actions") process.stdout.write(`✓ ${message}\n`);
|
|
482
|
+
else console.log(chalk.green(`\n✓ ${message}`));
|
|
118
483
|
}
|
|
119
484
|
function info(message) {
|
|
120
|
-
|
|
485
|
+
const mode = getOutputMode();
|
|
486
|
+
if (mode === "json") emitData({
|
|
487
|
+
type: "info",
|
|
488
|
+
message
|
|
489
|
+
});
|
|
490
|
+
else if (mode === "github-actions") process.stdout.write(`${message}\n`);
|
|
491
|
+
else console.log(chalk.cyan(`ℹ ${message}`));
|
|
492
|
+
}
|
|
493
|
+
function warn$1(message) {
|
|
494
|
+
const mode = getOutputMode();
|
|
495
|
+
if (mode === "json") emitData({
|
|
496
|
+
type: "warning",
|
|
497
|
+
message
|
|
498
|
+
});
|
|
499
|
+
else if (mode === "github-actions") process.stdout.write(`::warning::${message}\n`);
|
|
500
|
+
else console.log(chalk.yellow(`⚠ ${message}`));
|
|
121
501
|
}
|
|
122
|
-
|
|
123
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Emit structured data in JSON mode, or run the display callback otherwise.
|
|
504
|
+
*
|
|
505
|
+
* Usage:
|
|
506
|
+
* output({ type: "user", name, email }, () => {
|
|
507
|
+
* console.log(` Nome: ${name}`);
|
|
508
|
+
* console.log(` Email: ${email}`);
|
|
509
|
+
* });
|
|
510
|
+
*/
|
|
511
|
+
function output(data, display) {
|
|
512
|
+
if (isMachineReadable()) emitData(data);
|
|
513
|
+
else display();
|
|
124
514
|
}
|
|
125
515
|
function spinner(text) {
|
|
516
|
+
const mode = getOutputMode();
|
|
517
|
+
if (mode === "json" || mode === "github-actions" || mode === "plain") {
|
|
518
|
+
const noopSpinner = ora({
|
|
519
|
+
text,
|
|
520
|
+
isEnabled: false
|
|
521
|
+
});
|
|
522
|
+
const originalStop = noopSpinner.stop.bind(noopSpinner);
|
|
523
|
+
const originalFail = noopSpinner.fail.bind(noopSpinner);
|
|
524
|
+
noopSpinner.start = function(newText) {
|
|
525
|
+
if (newText) this.text = newText;
|
|
526
|
+
if (mode === "github-actions") process.stdout.write(`${this.text}\n`);
|
|
527
|
+
return this;
|
|
528
|
+
};
|
|
529
|
+
noopSpinner.stop = function() {
|
|
530
|
+
return originalStop();
|
|
531
|
+
};
|
|
532
|
+
noopSpinner.fail = function(newText) {
|
|
533
|
+
const msg = newText ?? this.text;
|
|
534
|
+
if (mode === "json") emitData({
|
|
535
|
+
type: "error",
|
|
536
|
+
message: msg
|
|
537
|
+
});
|
|
538
|
+
else if (mode === "github-actions") process.stdout.write(`::error::${msg}\n`);
|
|
539
|
+
else console.error(msg);
|
|
540
|
+
return originalFail(newText);
|
|
541
|
+
};
|
|
542
|
+
return noopSpinner;
|
|
543
|
+
}
|
|
126
544
|
return ora({
|
|
127
545
|
text,
|
|
128
546
|
color: "cyan"
|
|
129
547
|
}).start();
|
|
130
548
|
}
|
|
131
|
-
|
|
549
|
+
/**
|
|
550
|
+
* Print a table. Pass `rawData` for structured JSON output — commands
|
|
551
|
+
* build display rows with chalk as before, and raw objects for machines.
|
|
552
|
+
*
|
|
553
|
+
* Usage:
|
|
554
|
+
* printTable(
|
|
555
|
+
* ["Nome", "Email"],
|
|
556
|
+
* users.map(u => [chalk.bold(u.name), u.email]),
|
|
557
|
+
* users.map(u => ({ name: u.name, email: u.email })),
|
|
558
|
+
* );
|
|
559
|
+
*/
|
|
560
|
+
function printTable(headers, rows, rawData) {
|
|
561
|
+
const mode = getOutputMode();
|
|
562
|
+
if (mode === "json") {
|
|
563
|
+
if (rawData) emitData({
|
|
564
|
+
type: "table",
|
|
565
|
+
data: rawData
|
|
566
|
+
});
|
|
567
|
+
else emitData({
|
|
568
|
+
type: "table",
|
|
569
|
+
data: rows.map((row) => {
|
|
570
|
+
const obj = {};
|
|
571
|
+
headers.forEach((h, i) => {
|
|
572
|
+
obj[h] = stripAnsi(row[i] ?? "");
|
|
573
|
+
});
|
|
574
|
+
return obj;
|
|
575
|
+
})
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (mode === "github-actions" || mode === "plain") {
|
|
580
|
+
const cleanHeaders = headers.map(stripAnsi);
|
|
581
|
+
const cleanRows = rows.map((row) => row.map(stripAnsi));
|
|
582
|
+
const colWidths$1 = cleanHeaders.map((h, i) => {
|
|
583
|
+
const maxRow = cleanRows.reduce((max, row) => Math.max(max, (row[i] ?? "").length), 0);
|
|
584
|
+
return Math.max(h.length, maxRow);
|
|
585
|
+
});
|
|
586
|
+
const headerLine$1 = cleanHeaders.map((h, i) => h.padEnd(colWidths$1[i])).join(" ");
|
|
587
|
+
const separator$1 = colWidths$1.map((w) => "-".repeat(w)).join("--");
|
|
588
|
+
process.stdout.write(`\n${headerLine$1}\n`);
|
|
589
|
+
process.stdout.write(`${separator$1}\n`);
|
|
590
|
+
for (const row of cleanRows) {
|
|
591
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths$1[i])).join(" ");
|
|
592
|
+
process.stdout.write(`${line}\n`);
|
|
593
|
+
}
|
|
594
|
+
process.stdout.write("\n");
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
132
597
|
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);
|
|
598
|
+
const maxRow = rows.reduce((max, row) => Math.max(max, stripAnsi(row[i] ?? "").length), 0);
|
|
599
|
+
return Math.max(stripAnsi(h).length, maxRow);
|
|
135
600
|
});
|
|
136
601
|
const headerLine = headers.map((h, i) => chalk.bold(h.padEnd(colWidths[i]))).join(" ");
|
|
137
602
|
const separator = colWidths.map((w) => "─".repeat(w)).join("──");
|
|
138
603
|
console.log(`\n${headerLine}`);
|
|
139
604
|
console.log(chalk.dim(separator));
|
|
140
605
|
for (const row of rows) {
|
|
141
|
-
const line = row.map((cell, i) =>
|
|
606
|
+
const line = row.map((cell, i) => {
|
|
607
|
+
const visible = stripAnsi(cell);
|
|
608
|
+
const padding = colWidths[i] - visible.length;
|
|
609
|
+
return cell + " ".repeat(Math.max(0, padding));
|
|
610
|
+
}).join(" ");
|
|
142
611
|
console.log(line);
|
|
143
612
|
}
|
|
144
613
|
console.log();
|
|
145
614
|
}
|
|
146
615
|
async function prompt(question) {
|
|
616
|
+
if (!isInteractive()) return "";
|
|
147
617
|
const rl = createInterface({
|
|
148
618
|
input: process.stdin,
|
|
149
619
|
output: process.stdout
|
|
@@ -156,11 +626,17 @@ async function prompt(question) {
|
|
|
156
626
|
});
|
|
157
627
|
}
|
|
158
628
|
async function promptConfirm(question, defaultYes = true) {
|
|
629
|
+
if (!isInteractive()) return defaultYes;
|
|
159
630
|
const answer = await prompt(`${question} ${defaultYes ? "(S/n)" : "(s/N)"}`);
|
|
160
631
|
if (answer === "") return defaultYes;
|
|
161
632
|
return answer.toLowerCase().startsWith("s") || answer.toLowerCase().startsWith("y");
|
|
162
633
|
}
|
|
163
634
|
async function promptSelect(question, options) {
|
|
635
|
+
if (!isInteractive()) {
|
|
636
|
+
if (options.length > 0) return options[0].value;
|
|
637
|
+
console.error(chalk.red("Nenhuma opção disponível no modo não-interativo."));
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
164
640
|
console.log(chalk.cyan(`\n${question}\n`));
|
|
165
641
|
for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
|
|
166
642
|
const answer = await prompt("\nEscolha (número):");
|
|
@@ -172,6 +648,7 @@ async function promptSelect(question, options) {
|
|
|
172
648
|
return options[index].value;
|
|
173
649
|
}
|
|
174
650
|
async function promptMultiSelect(question, options) {
|
|
651
|
+
if (!isInteractive()) return options.map((o) => o.value);
|
|
175
652
|
console.log(chalk.cyan(`\n${question}\n`));
|
|
176
653
|
for (let i = 0; i < options.length; i++) console.log(chalk.white(` ${chalk.bold(`${i + 1})`)} ${options[i].label}`));
|
|
177
654
|
console.log(chalk.dim(`\n * = todos`));
|
|
@@ -184,6 +661,10 @@ async function promptMultiSelect(question, options) {
|
|
|
184
661
|
}
|
|
185
662
|
return indices.map((i) => options[i].value);
|
|
186
663
|
}
|
|
664
|
+
const ANSI_REGEX = /\x1B\[[0-9;]*m/g;
|
|
665
|
+
function stripAnsi(str) {
|
|
666
|
+
return str.replace(ANSI_REGEX, "");
|
|
667
|
+
}
|
|
187
668
|
|
|
188
669
|
//#endregion
|
|
189
670
|
//#region src/commands/login.ts
|
|
@@ -206,15 +687,21 @@ async function performLogin(apiUrl) {
|
|
|
206
687
|
}
|
|
207
688
|
s.stop();
|
|
208
689
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
690
|
+
output({
|
|
691
|
+
type: "auth_code",
|
|
692
|
+
userCode: data.user_code,
|
|
693
|
+
verificationUrl
|
|
694
|
+
}, () => {
|
|
695
|
+
console.log();
|
|
696
|
+
info("Abrindo navegador para autenticação...");
|
|
697
|
+
console.log();
|
|
698
|
+
console.log(chalk.white(` Código de verificação: ${chalk.bold.cyan(data.user_code)}`));
|
|
699
|
+
console.log();
|
|
700
|
+
console.log(chalk.dim(" Se o navegador não abrir, acesse manualmente:"));
|
|
701
|
+
console.log(chalk.dim(` ${verificationUrl}`));
|
|
702
|
+
console.log();
|
|
703
|
+
openBrowser(verificationUrl);
|
|
704
|
+
});
|
|
218
705
|
const pollSpinner = spinner("Aguardando autorização no navegador...");
|
|
219
706
|
const token = await pollForToken(authClient, data.device_code, data.interval || 5);
|
|
220
707
|
if (!token) {
|
|
@@ -229,18 +716,17 @@ async function performLogin(apiUrl) {
|
|
|
229
716
|
success("Autenticado com sucesso!");
|
|
230
717
|
} catch (err) {
|
|
231
718
|
s.stop();
|
|
232
|
-
if (err instanceof Error
|
|
233
|
-
|
|
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);
|
|
719
|
+
if (err instanceof Error) handleLoginError(err);
|
|
720
|
+
throw err;
|
|
239
721
|
}
|
|
240
722
|
}
|
|
723
|
+
function handleLoginError(err) {
|
|
724
|
+
if (err.message.includes("fetch") || err.message.includes("ECONNREFUSED")) throw err;
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
241
727
|
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
728
|
if (isAuthenticated()) {
|
|
243
|
-
warn("Você já está autenticado.");
|
|
729
|
+
warn$1("Você já está autenticado.");
|
|
244
730
|
if ((await prompt("Deseja fazer login novamente? (s/N)")).toLowerCase() !== "s") process.exit(0);
|
|
245
731
|
}
|
|
246
732
|
const config = loadConfig$1();
|
|
@@ -278,7 +764,7 @@ async function pollForToken(authClient, deviceCode, interval) {
|
|
|
278
764
|
}
|
|
279
765
|
const logoutCommand = new Command("logout").description("Encerrar sessão na plataforma Veloz").action(() => {
|
|
280
766
|
if (!isAuthenticated()) {
|
|
281
|
-
warn("Você não está autenticado.");
|
|
767
|
+
warn$1("Você não está autenticado.");
|
|
282
768
|
return;
|
|
283
769
|
}
|
|
284
770
|
deleteConfig();
|
|
@@ -288,660 +774,329 @@ const logoutCommand = new Command("logout").description("Encerrar sessão na pla
|
|
|
288
774
|
//#endregion
|
|
289
775
|
//#region ../../packages/api/src/client.ts
|
|
290
776
|
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
|
-
}
|
|
777
|
+
return createORPCClient(new RPCLink({
|
|
778
|
+
url: `${baseUrl}/rpc`,
|
|
779
|
+
headers: headers ?? (() => ({})),
|
|
780
|
+
fetch: (request, init) => {
|
|
781
|
+
return globalThis.fetch(request, {
|
|
782
|
+
...init,
|
|
783
|
+
credentials: "include"
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}));
|
|
538
787
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
function
|
|
788
|
+
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/lib/client.ts
|
|
791
|
+
async function getClient() {
|
|
792
|
+
const authConfig = await requireAuth();
|
|
793
|
+
const projectConfig = loadConfig();
|
|
794
|
+
return createClient(authConfig.apiUrl, () => {
|
|
795
|
+
const headers = { Authorization: `Bearer ${authConfig.apiKey}` };
|
|
796
|
+
if (projectConfig?.project?.id) headers["X-Project-Id"] = projectConfig.project.id;
|
|
797
|
+
if (!projectConfig?.project?.id && authConfig.organizationId) headers["X-Organization-Id"] = authConfig.organizationId;
|
|
798
|
+
return headers;
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region src/commands/projects.ts
|
|
804
|
+
const projectsCommand = new Command("projects").alias("projetos").description("Gerenciar projetos");
|
|
805
|
+
projectsCommand.command("list").alias("listar").description("Listar todos os projetos").action(async () => {
|
|
806
|
+
const spin = spinner("Carregando projetos...");
|
|
543
807
|
try {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
808
|
+
const projects = await (await getClient()).projects.list();
|
|
809
|
+
spin.stop();
|
|
810
|
+
if (projects.length === 0) {
|
|
811
|
+
info("Nenhum projeto encontrado. Crie um pelo dashboard.");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
printTable([
|
|
815
|
+
"ID",
|
|
816
|
+
"Nome",
|
|
817
|
+
"Slug",
|
|
818
|
+
"Repo GitHub",
|
|
819
|
+
"Criado em"
|
|
820
|
+
], projects.map((p) => [
|
|
821
|
+
p.id,
|
|
822
|
+
p.name,
|
|
823
|
+
p.slug,
|
|
824
|
+
p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
|
|
825
|
+
new Date(p.createdAt).toLocaleDateString("pt-BR")
|
|
826
|
+
]), projects.map((p) => ({
|
|
827
|
+
id: p.id,
|
|
828
|
+
name: p.name,
|
|
829
|
+
slug: p.slug,
|
|
830
|
+
githubRepo: p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : null,
|
|
831
|
+
createdAt: p.createdAt
|
|
832
|
+
})));
|
|
833
|
+
} catch (error) {
|
|
834
|
+
spin.stop();
|
|
835
|
+
handleError(error);
|
|
547
836
|
}
|
|
548
|
-
}
|
|
837
|
+
});
|
|
549
838
|
|
|
550
839
|
//#endregion
|
|
551
840
|
//#region src/commands/link.ts
|
|
552
841
|
const linkCommand = new Command("link").description("Verificar vínculo do projeto com Veloz").action(async () => {
|
|
553
842
|
const config = loadConfig();
|
|
554
843
|
if (!config) {
|
|
555
|
-
warn("Nenhum projeto configurado. Execute 'veloz deploy' para configurar seu projeto.");
|
|
844
|
+
warn$1("Nenhum projeto configurado. Execute 'veloz deploy' para configurar seu projeto.");
|
|
556
845
|
return;
|
|
557
846
|
}
|
|
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
847
|
const services = Object.entries(config.services);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
848
|
+
output({
|
|
849
|
+
type: "link",
|
|
850
|
+
project: {
|
|
851
|
+
id: config.project.id,
|
|
852
|
+
name: config.project.name
|
|
853
|
+
},
|
|
854
|
+
services: services.map(([key, service]) => ({
|
|
855
|
+
key,
|
|
856
|
+
id: service.id,
|
|
857
|
+
name: service.name,
|
|
858
|
+
type: service.type
|
|
859
|
+
}))
|
|
860
|
+
}, () => {
|
|
861
|
+
console.log(chalk.bold("\n🔗 Projeto Vinculado\n"));
|
|
862
|
+
console.log(` ${chalk.bold("Projeto:")} ${chalk.cyan(config.project.name)}`);
|
|
863
|
+
console.log(` ${chalk.bold("ID:")} ${chalk.dim(config.project.id)}`);
|
|
864
|
+
if (services.length > 0) {
|
|
865
|
+
console.log(`\n ${chalk.bold("Serviços:")}`);
|
|
866
|
+
services.forEach(([key, service]) => {
|
|
867
|
+
const type = service.type === "web" ? "Serviço Web" : "Site Estático";
|
|
868
|
+
console.log(` • ${chalk.cyan(service.name)} (${key}) - ${type}`);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
console.log();
|
|
872
|
+
info("Use 'veloz deploy' para fazer deploy ou atualizar serviços.");
|
|
873
|
+
});
|
|
571
874
|
});
|
|
572
875
|
|
|
573
876
|
//#endregion
|
|
574
877
|
//#region ../../packages/api/src/lib/framework-detector.ts
|
|
575
|
-
|
|
576
|
-
{
|
|
878
|
+
function safeParsePkg(content) {
|
|
879
|
+
try {
|
|
880
|
+
return JSON.parse(content);
|
|
881
|
+
} catch {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function pmRun(pm, script) {
|
|
886
|
+
if (pm === "yarn") return `yarn ${script}`;
|
|
887
|
+
return `${pm} run ${script}`;
|
|
888
|
+
}
|
|
889
|
+
function detectFramework(pkgJsonStr, pm) {
|
|
890
|
+
const pkg = safeParsePkg(pkgJsonStr);
|
|
891
|
+
if (!pkg) return null;
|
|
892
|
+
const allDeps = {
|
|
893
|
+
...pkg.dependencies,
|
|
894
|
+
...pkg.devDependencies
|
|
895
|
+
};
|
|
896
|
+
const hasReact = !!allDeps["react"];
|
|
897
|
+
if (allDeps["next"]) return {
|
|
577
898
|
name: "nextjs",
|
|
578
899
|
label: "Next.js",
|
|
579
900
|
type: "WEB",
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
startCommand: "npm start",
|
|
901
|
+
buildCommand: pmRun(pm, "build"),
|
|
902
|
+
startCommand: pmRun(pm, "start"),
|
|
583
903
|
outputDir: ".next",
|
|
584
904
|
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
|
-
{
|
|
905
|
+
};
|
|
906
|
+
if (allDeps["nuxt"] || allDeps["nuxt3"]) return {
|
|
597
907
|
name: "nuxt",
|
|
598
908
|
label: "Nuxt",
|
|
599
909
|
type: "WEB",
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
outputDir: ".nuxt",
|
|
910
|
+
buildCommand: pmRun(pm, "build"),
|
|
911
|
+
startCommand: pmRun(pm, "start"),
|
|
912
|
+
outputDir: ".output",
|
|
604
913
|
port: 3e3
|
|
605
|
-
}
|
|
606
|
-
{
|
|
914
|
+
};
|
|
915
|
+
if (allDeps["@remix-run/node"] || allDeps["@remix-run/react"]) return {
|
|
607
916
|
name: "remix",
|
|
608
917
|
label: "Remix",
|
|
609
918
|
type: "WEB",
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
startCommand: "npm start",
|
|
919
|
+
buildCommand: pmRun(pm, "build"),
|
|
920
|
+
startCommand: pmRun(pm, "start"),
|
|
613
921
|
outputDir: "build",
|
|
614
922
|
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
|
-
{
|
|
923
|
+
};
|
|
924
|
+
if (allDeps["astro"]) return {
|
|
627
925
|
name: "astro",
|
|
628
926
|
label: "Astro",
|
|
629
927
|
type: "STATIC",
|
|
630
|
-
|
|
631
|
-
buildCommand: "npm run build",
|
|
928
|
+
buildCommand: pmRun(pm, "build"),
|
|
632
929
|
startCommand: null,
|
|
633
930
|
outputDir: "dist",
|
|
634
931
|
port: 3e3
|
|
635
|
-
}
|
|
636
|
-
{
|
|
932
|
+
};
|
|
933
|
+
if (allDeps["@sveltejs/kit"]) return {
|
|
934
|
+
name: "sveltekit",
|
|
935
|
+
label: "SvelteKit",
|
|
936
|
+
type: "WEB",
|
|
937
|
+
buildCommand: pmRun(pm, "build"),
|
|
938
|
+
startCommand: pmRun(pm, "preview"),
|
|
939
|
+
outputDir: "build",
|
|
940
|
+
port: 3e3
|
|
941
|
+
};
|
|
942
|
+
if (allDeps["gatsby"]) return {
|
|
637
943
|
name: "gatsby",
|
|
638
944
|
label: "Gatsby",
|
|
639
945
|
type: "STATIC",
|
|
640
|
-
|
|
641
|
-
buildCommand: "npm run build",
|
|
946
|
+
buildCommand: pmRun(pm, "build"),
|
|
642
947
|
startCommand: null,
|
|
643
948
|
outputDir: "public",
|
|
644
949
|
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
|
-
{
|
|
950
|
+
};
|
|
951
|
+
if (allDeps["@angular/core"]) return {
|
|
687
952
|
name: "angular",
|
|
688
953
|
label: "Angular",
|
|
689
|
-
type: "
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
startCommand: null,
|
|
954
|
+
type: "WEB",
|
|
955
|
+
buildCommand: pmRun(pm, "build"),
|
|
956
|
+
startCommand: pmRun(pm, "start"),
|
|
693
957
|
outputDir: "dist",
|
|
694
958
|
port: 4200
|
|
695
|
-
}
|
|
696
|
-
{
|
|
959
|
+
};
|
|
960
|
+
if (allDeps["hono"]) return {
|
|
961
|
+
name: "hono",
|
|
962
|
+
label: "Hono",
|
|
963
|
+
type: "WEB",
|
|
964
|
+
buildCommand: pmRun(pm, "build"),
|
|
965
|
+
startCommand: pmRun(pm, "start"),
|
|
966
|
+
outputDir: null,
|
|
967
|
+
port: 3e3
|
|
968
|
+
};
|
|
969
|
+
if (allDeps["express"]) return {
|
|
697
970
|
name: "express",
|
|
698
971
|
label: "Express",
|
|
699
972
|
type: "WEB",
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
startCommand: "node index.js",
|
|
973
|
+
buildCommand: pmRun(pm, "build"),
|
|
974
|
+
startCommand: pmRun(pm, "start"),
|
|
703
975
|
outputDir: null,
|
|
704
976
|
port: 3e3
|
|
705
|
-
}
|
|
706
|
-
{
|
|
977
|
+
};
|
|
978
|
+
if (allDeps["fastify"]) return {
|
|
707
979
|
name: "fastify",
|
|
708
980
|
label: "Fastify",
|
|
709
981
|
type: "WEB",
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
startCommand: "node index.js",
|
|
982
|
+
buildCommand: pmRun(pm, "build"),
|
|
983
|
+
startCommand: pmRun(pm, "start"),
|
|
713
984
|
outputDir: null,
|
|
714
985
|
port: 3e3
|
|
715
|
-
}
|
|
716
|
-
{
|
|
986
|
+
};
|
|
987
|
+
if (allDeps["@nestjs/core"]) return {
|
|
717
988
|
name: "nestjs",
|
|
718
989
|
label: "NestJS",
|
|
719
990
|
type: "WEB",
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
startCommand: "npm run start:prod",
|
|
991
|
+
buildCommand: pmRun(pm, "build"),
|
|
992
|
+
startCommand: pmRun(pm, "start:prod"),
|
|
723
993
|
outputDir: "dist",
|
|
724
994
|
port: 3e3
|
|
725
|
-
}
|
|
726
|
-
{
|
|
727
|
-
name: "
|
|
728
|
-
label: "
|
|
995
|
+
};
|
|
996
|
+
if (allDeps["vite"]) return {
|
|
997
|
+
name: hasReact ? "vite-react" : "vite",
|
|
998
|
+
label: hasReact ? "Vite + React" : "Vite",
|
|
999
|
+
type: "STATIC",
|
|
1000
|
+
buildCommand: pmRun(pm, "build"),
|
|
1001
|
+
startCommand: null,
|
|
1002
|
+
outputDir: "dist",
|
|
1003
|
+
port: 3e3
|
|
1004
|
+
};
|
|
1005
|
+
if (allDeps["react-scripts"]) return {
|
|
1006
|
+
name: "cra",
|
|
1007
|
+
label: "Create React App",
|
|
1008
|
+
type: "STATIC",
|
|
1009
|
+
buildCommand: pmRun(pm, "build"),
|
|
1010
|
+
startCommand: null,
|
|
1011
|
+
outputDir: "build",
|
|
1012
|
+
port: 3e3
|
|
1013
|
+
};
|
|
1014
|
+
if (pkg.scripts?.build || pkg.scripts?.start) return {
|
|
1015
|
+
name: "node",
|
|
1016
|
+
label: "Node.js",
|
|
729
1017
|
type: "WEB",
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
outputDir: null,
|
|
1018
|
+
buildCommand: pkg.scripts?.build ? pmRun(pm, "build") : "",
|
|
1019
|
+
startCommand: pkg.scripts?.start ? pmRun(pm, "start") : "node index.js",
|
|
1020
|
+
outputDir: "dist",
|
|
734
1021
|
port: 3e3
|
|
735
|
-
}
|
|
736
|
-
|
|
1022
|
+
};
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
737
1025
|
function detectPackageManager$1(files) {
|
|
738
|
-
if ("
|
|
739
|
-
if ("pnpm-lock.yaml" in files || "pnpm-workspace.yaml" in files) return "pnpm";
|
|
1026
|
+
if ("pnpm-lock.yaml" in files) return "pnpm";
|
|
740
1027
|
if ("yarn.lock" in files) return "yarn";
|
|
1028
|
+
if ("bun.lock" in files || "bun.lockb" in files) return "bun";
|
|
741
1029
|
return "npm";
|
|
742
1030
|
}
|
|
743
|
-
function
|
|
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 [];
|
|
1031
|
+
function extractEnvVars(files) {
|
|
824
1032
|
const vars = [];
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
if (
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const parts = filePath.slice(base.length + 1).split("/");
|
|
847
|
-
if (parts.length === 2 && parts[1] === "package.json") dirs.add(base + "/" + parts[0]);
|
|
1033
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1034
|
+
for (const key of [
|
|
1035
|
+
".env.example",
|
|
1036
|
+
".env.sample",
|
|
1037
|
+
".env"
|
|
1038
|
+
]) {
|
|
1039
|
+
const content = files[key];
|
|
1040
|
+
if (!content) continue;
|
|
1041
|
+
for (const line of content.split("\n")) {
|
|
1042
|
+
const trimmed = line.trim();
|
|
1043
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1044
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1045
|
+
if (eqIdx > 0) {
|
|
1046
|
+
const envKey = trimmed.slice(0, eqIdx).trim();
|
|
1047
|
+
if (!seen.has(envKey)) {
|
|
1048
|
+
seen.add(envKey);
|
|
1049
|
+
vars.push({
|
|
1050
|
+
key: envKey,
|
|
1051
|
+
value: trimmed.slice(eqIdx + 1)
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
848
1054
|
}
|
|
849
|
-
} else {
|
|
850
|
-
const pkgPath = base + "/package.json";
|
|
851
|
-
if (availableFiles.includes(pkgPath)) dirs.add(base);
|
|
852
1055
|
}
|
|
853
1056
|
}
|
|
854
|
-
return
|
|
1057
|
+
return vars;
|
|
855
1058
|
}
|
|
856
|
-
function
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
}
|
|
1059
|
+
function analyzeRepo(files) {
|
|
1060
|
+
const packageManager = detectPackageManager$1(files);
|
|
1061
|
+
const envVars = extractEnvVars(files);
|
|
1062
|
+
const rootPkgContent = files["package.json"];
|
|
1063
|
+
const rootPkg = rootPkgContent ? safeParsePkg(rootPkgContent) : null;
|
|
1064
|
+
const isMonorepo = "pnpm-workspace.yaml" in files || !!rootPkg?.workspaces;
|
|
1065
|
+
const framework = rootPkgContent ? detectFramework(rootPkgContent, packageManager) : null;
|
|
1066
|
+
const monorepoApps = [];
|
|
1067
|
+
if (isMonorepo) for (const [filePath, content] of Object.entries(files)) {
|
|
1068
|
+
if (filePath === "package.json") continue;
|
|
1069
|
+
if (!filePath.endsWith("/package.json")) continue;
|
|
1070
|
+
const nested = safeParsePkg(content);
|
|
1071
|
+
if (!nested) continue;
|
|
1072
|
+
const appPath = filePath.replace("/package.json", "");
|
|
1073
|
+
const appName = nested.name || appPath.split("/").pop() || appPath;
|
|
1074
|
+
let appFramework = detectFramework(content, packageManager);
|
|
1075
|
+
if (appFramework && rootPkg) {
|
|
1076
|
+
if (rootPkg.scripts?.[`build:${appName}`]) appFramework = {
|
|
1077
|
+
...appFramework,
|
|
1078
|
+
buildCommand: pmRun(packageManager, `build:${appName}`)
|
|
1079
|
+
};
|
|
1080
|
+
if (nested.scripts?.start?.startsWith("node ")) {
|
|
1081
|
+
const nodePath = nested.scripts.start.slice(5).trim();
|
|
1082
|
+
appFramework = {
|
|
1083
|
+
...appFramework,
|
|
1084
|
+
startCommand: `node ${appPath}/${nodePath}`
|
|
1085
|
+
};
|
|
921
1086
|
}
|
|
922
1087
|
}
|
|
923
|
-
|
|
924
|
-
name,
|
|
925
|
-
path:
|
|
926
|
-
framework
|
|
1088
|
+
monorepoApps.push({
|
|
1089
|
+
name: appName,
|
|
1090
|
+
path: appPath,
|
|
1091
|
+
framework: appFramework
|
|
927
1092
|
});
|
|
928
1093
|
}
|
|
929
1094
|
return {
|
|
930
|
-
|
|
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 {
|
|
1095
|
+
packageManager,
|
|
940
1096
|
framework,
|
|
941
|
-
packageManager: pm,
|
|
942
1097
|
envVars,
|
|
943
1098
|
isMonorepo,
|
|
944
|
-
monorepoApps
|
|
1099
|
+
monorepoApps
|
|
945
1100
|
};
|
|
946
1101
|
}
|
|
947
1102
|
|
|
@@ -1001,170 +1156,108 @@ async function getFilesToUpload(directory) {
|
|
|
1001
1156
|
return files;
|
|
1002
1157
|
}
|
|
1003
1158
|
async function createTarball(directory, extraFiles) {
|
|
1004
|
-
const
|
|
1159
|
+
const tempDir = await mkdtemp(join(tmpdir(), "veloz-upload-"));
|
|
1160
|
+
const tarPath = join(tempDir, "source.tar.gz");
|
|
1161
|
+
const injectedFiles = [];
|
|
1005
1162
|
if (extraFiles) for (const file of extraFiles) {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
writeFileSync(filePath, file.content);
|
|
1009
|
-
createdFiles.push(filePath);
|
|
1010
|
-
}
|
|
1163
|
+
if (existsSync(join(directory, file.name))) continue;
|
|
1164
|
+
injectedFiles.push(file);
|
|
1011
1165
|
}
|
|
1012
1166
|
try {
|
|
1013
1167
|
const files = await getFilesToUpload(directory);
|
|
1014
1168
|
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
1169
|
const relativePaths = files.map((f) => relative(directory, f));
|
|
1017
|
-
|
|
1018
|
-
const rel = relative(directory, f);
|
|
1019
|
-
if (!relativePaths.includes(rel)) relativePaths.push(rel);
|
|
1020
|
-
}
|
|
1021
|
-
await tar.create({
|
|
1170
|
+
if (injectedFiles.length === 0) await tar.create({
|
|
1022
1171
|
gzip: true,
|
|
1023
1172
|
file: tarPath,
|
|
1024
1173
|
cwd: directory
|
|
1025
1174
|
}, relativePaths);
|
|
1175
|
+
else {
|
|
1176
|
+
const stagingDir = join(tempDir, "staging");
|
|
1177
|
+
await mkdir(stagingDir, { recursive: true });
|
|
1178
|
+
for (const rel of relativePaths) {
|
|
1179
|
+
const src = join(directory, rel);
|
|
1180
|
+
const dest = join(stagingDir, rel);
|
|
1181
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
1182
|
+
await link(src, dest);
|
|
1183
|
+
}
|
|
1184
|
+
for (const file of injectedFiles) {
|
|
1185
|
+
writeFileSync(join(stagingDir, file.name), file.content);
|
|
1186
|
+
if (!relativePaths.includes(file.name)) relativePaths.push(file.name);
|
|
1187
|
+
}
|
|
1188
|
+
await tar.create({
|
|
1189
|
+
gzip: true,
|
|
1190
|
+
file: tarPath,
|
|
1191
|
+
cwd: stagingDir
|
|
1192
|
+
}, relativePaths);
|
|
1193
|
+
}
|
|
1026
1194
|
return {
|
|
1027
1195
|
path: tarPath,
|
|
1028
|
-
size: statSync(tarPath).size
|
|
1196
|
+
size: statSync(tarPath).size,
|
|
1197
|
+
tempDir
|
|
1029
1198
|
};
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
await rm(tempDir, {
|
|
1201
|
+
recursive: true,
|
|
1202
|
+
force: true
|
|
1203
|
+
}).catch(() => {});
|
|
1204
|
+
throw err;
|
|
1034
1205
|
}
|
|
1035
1206
|
}
|
|
1036
|
-
async function uploadSource(
|
|
1037
|
-
const { path: tarPath } = await createTarball(directory, extraFiles);
|
|
1207
|
+
async function uploadSource(deploymentId, directory, extraFiles) {
|
|
1208
|
+
const { path: tarPath, tempDir } = await createTarball(directory, extraFiles);
|
|
1209
|
+
const client = await getClient();
|
|
1038
1210
|
try {
|
|
1039
|
-
const
|
|
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();
|
|
1211
|
+
const { uploadUrl, objectKey } = await client.deployments.getUploadUrl({ deploymentId });
|
|
1051
1212
|
const fileBuffer = readFileSync(tarPath);
|
|
1052
1213
|
const putResponse = await fetch(uploadUrl, {
|
|
1053
1214
|
method: "PUT",
|
|
1054
1215
|
headers: { "Content-Type": "application/gzip" },
|
|
1055
1216
|
body: fileBuffer
|
|
1056
1217
|
});
|
|
1057
|
-
if (!putResponse.ok) throw new Error(`
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
Authorization: `Bearer ${token}`,
|
|
1062
|
-
"Content-Type": "application/json"
|
|
1063
|
-
},
|
|
1064
|
-
body: JSON.stringify({ objectKey })
|
|
1218
|
+
if (!putResponse.ok) throw new Error(`Upload falhou: ${putResponse.status}`);
|
|
1219
|
+
await client.deployments.startBuild({
|
|
1220
|
+
deploymentId,
|
|
1221
|
+
objectKey
|
|
1065
1222
|
});
|
|
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
1223
|
} finally {
|
|
1071
|
-
await rm(
|
|
1072
|
-
|
|
1073
|
-
|
|
1224
|
+
await rm(tempDir, {
|
|
1225
|
+
recursive: true,
|
|
1226
|
+
force: true
|
|
1074
1227
|
}).catch(() => {});
|
|
1075
1228
|
}
|
|
1076
1229
|
}
|
|
1077
1230
|
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);
|
|
1231
|
+
const files = await getFilesToUpload(directory);
|
|
1232
|
+
let totalSize = 0;
|
|
1233
|
+
for (const file of files) {
|
|
1234
|
+
const stats = await stat(file);
|
|
1235
|
+
totalSize += stats.size;
|
|
1150
1236
|
}
|
|
1237
|
+
return totalSize;
|
|
1151
1238
|
}
|
|
1152
1239
|
|
|
1153
1240
|
//#endregion
|
|
1154
|
-
//#region src/lib/
|
|
1155
|
-
async function withRetry
|
|
1241
|
+
//#region src/lib/retry.ts
|
|
1242
|
+
async function withRetry(fn, maxRetries = 3) {
|
|
1156
1243
|
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
1157
1244
|
return await fn();
|
|
1158
1245
|
} catch (error) {
|
|
1159
|
-
if (attempt
|
|
1246
|
+
if (attempt >= maxRetries) throw error;
|
|
1247
|
+
const rateLimit = isRateLimitError(error);
|
|
1248
|
+
if (rateLimit) {
|
|
1249
|
+
const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
|
|
1250
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
1251
|
+
} else {
|
|
1160
1252
|
const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
|
|
1161
|
-
await new Promise((
|
|
1162
|
-
continue;
|
|
1253
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1163
1254
|
}
|
|
1164
|
-
throw error;
|
|
1165
1255
|
}
|
|
1166
1256
|
throw new Error("Max retries exceeded");
|
|
1167
1257
|
}
|
|
1258
|
+
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/lib/deploy-constants.ts
|
|
1168
1261
|
const statusLabels = {
|
|
1169
1262
|
QUEUED: "Na fila",
|
|
1170
1263
|
BUILDING: "Construindo",
|
|
@@ -1174,21 +1267,90 @@ const statusLabels = {
|
|
|
1174
1267
|
FAILED: "Falhou",
|
|
1175
1268
|
CANCELLED: "Cancelado"
|
|
1176
1269
|
};
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1270
|
+
function makeStatusIcons() {
|
|
1271
|
+
const mode = getOutputMode();
|
|
1272
|
+
if (mode === "json" || mode === "github-actions" || mode === "plain") return {
|
|
1273
|
+
QUEUED: "○",
|
|
1274
|
+
BUILDING: "●",
|
|
1275
|
+
DEPLOYING: "●",
|
|
1276
|
+
LIVE: "●",
|
|
1277
|
+
BUILD_FAILED: "●",
|
|
1278
|
+
FAILED: "●",
|
|
1279
|
+
CANCELLED: "●"
|
|
1280
|
+
};
|
|
1281
|
+
return {
|
|
1282
|
+
QUEUED: chalk.gray("○"),
|
|
1283
|
+
BUILDING: chalk.yellow("●"),
|
|
1284
|
+
DEPLOYING: chalk.blue("●"),
|
|
1285
|
+
LIVE: chalk.green("●"),
|
|
1286
|
+
BUILD_FAILED: chalk.red("●"),
|
|
1287
|
+
FAILED: chalk.red("●"),
|
|
1288
|
+
CANCELLED: chalk.gray("●")
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const statusIcons = new Proxy({}, { get(_target, prop) {
|
|
1292
|
+
return makeStatusIcons()[prop] ?? chalk.yellow("●");
|
|
1293
|
+
} });
|
|
1186
1294
|
const TERMINAL_STATUSES = new Set([
|
|
1187
1295
|
"LIVE",
|
|
1188
1296
|
"BUILD_FAILED",
|
|
1189
1297
|
"FAILED",
|
|
1190
1298
|
"CANCELLED"
|
|
1191
1299
|
]);
|
|
1300
|
+
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/lib/deploy-cancel.ts
|
|
1303
|
+
const activeDeploymentIds = /* @__PURE__ */ new Set();
|
|
1304
|
+
let sigintHandlerRegistered = false;
|
|
1305
|
+
function trackDeployment(deploymentId) {
|
|
1306
|
+
activeDeploymentIds.add(deploymentId);
|
|
1307
|
+
}
|
|
1308
|
+
function untrackDeployment(deploymentId) {
|
|
1309
|
+
activeDeploymentIds.delete(deploymentId);
|
|
1310
|
+
}
|
|
1311
|
+
function setupSigintHandler() {
|
|
1312
|
+
if (sigintHandlerRegistered) return;
|
|
1313
|
+
sigintHandlerRegistered = true;
|
|
1314
|
+
process.on("SIGINT", async () => {
|
|
1315
|
+
if (activeDeploymentIds.size === 0) process.exit(130);
|
|
1316
|
+
const mode = getOutputMode();
|
|
1317
|
+
if (mode === "json") process.stdout.write(JSON.stringify({ type: "deploy_cancelled" }) + "\n");
|
|
1318
|
+
else console.log(chalk.yellow("\n\nCancelando deploy(s)..."));
|
|
1319
|
+
try {
|
|
1320
|
+
const client = await getClient();
|
|
1321
|
+
const cancelPromises = Array.from(activeDeploymentIds).map((deploymentId) => client.deployments.cancel({ deploymentId }).catch(() => {}));
|
|
1322
|
+
await Promise.all(cancelPromises);
|
|
1323
|
+
if (mode !== "json") console.log(chalk.yellow("Deploy cancelado."));
|
|
1324
|
+
} catch {}
|
|
1325
|
+
process.exit(130);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
//#endregion
|
|
1330
|
+
//#region src/lib/deploy-parallel.ts
|
|
1331
|
+
async function fetchDeployUrls$1(client, serviceId) {
|
|
1332
|
+
try {
|
|
1333
|
+
return (await client.domains.list({ serviceId })).map((d) => `https://${d.domain}`);
|
|
1334
|
+
} catch {
|
|
1335
|
+
return [];
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function getFailureHints$1(status) {
|
|
1339
|
+
switch (status) {
|
|
1340
|
+
case "BUILD_FAILED": return [
|
|
1341
|
+
"Verifique os logs de build acima para erros de compilação",
|
|
1342
|
+
"Teste o build localmente: rode o comando de build do seu projeto",
|
|
1343
|
+
"Use 'veloz config show' para verificar as configurações"
|
|
1344
|
+
];
|
|
1345
|
+
case "DEPLOY_FAILED": return [
|
|
1346
|
+
"O build passou mas o serviço falhou ao iniciar",
|
|
1347
|
+
"Verifique se a porta configurada está correta: 'veloz config show'",
|
|
1348
|
+
"Veja os logs de runtime: 'veloz logs -f'"
|
|
1349
|
+
];
|
|
1350
|
+
case "CANCELLED": return ["Deploy cancelado. Execute 'veloz deploy' para tentar novamente."];
|
|
1351
|
+
default: return ["Execute 'veloz logs -f' para mais detalhes."];
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1192
1354
|
function renderProgress(progressMap, prevLineCount) {
|
|
1193
1355
|
for (let i = 0; i < prevLineCount; i++) process.stdout.write("\x1B[1A\x1B[2K");
|
|
1194
1356
|
let lineCount = 0;
|
|
@@ -1198,35 +1360,47 @@ function renderProgress(progressMap, prevLineCount) {
|
|
|
1198
1360
|
process.stdout.write(`${icon} ${chalk.bold(progress.serviceName)}: ${label}\n`);
|
|
1199
1361
|
lineCount++;
|
|
1200
1362
|
if (progress.status === "BUILDING" || progress.status === "BUILD_FAILED") {
|
|
1201
|
-
|
|
1202
|
-
|
|
1363
|
+
const nonEmptyLines = progress.logLines.filter((l) => l.trim());
|
|
1364
|
+
if (nonEmptyLines.length > 0) {
|
|
1365
|
+
const tail = nonEmptyLines.slice(-3);
|
|
1203
1366
|
for (const line of tail) {
|
|
1204
1367
|
const truncated = line.substring(0, 80) + (line.length > 80 ? "..." : "");
|
|
1205
1368
|
process.stdout.write(` ${chalk.dim(truncated)}\n`);
|
|
1206
1369
|
lineCount++;
|
|
1207
1370
|
}
|
|
1208
1371
|
} else if (progress.status === "BUILDING") {
|
|
1209
|
-
process.stdout.write(` ${chalk.dim("
|
|
1372
|
+
process.stdout.write(` ${chalk.dim("Aguardando logs do build...")}\n`);
|
|
1210
1373
|
lineCount++;
|
|
1211
1374
|
}
|
|
1212
1375
|
} else if (progress.status === "QUEUED") {
|
|
1213
|
-
process.stdout.write(` ${chalk.dim("
|
|
1376
|
+
process.stdout.write(` ${chalk.dim("Na fila para construção...")}\n`);
|
|
1214
1377
|
lineCount++;
|
|
1215
1378
|
}
|
|
1216
1379
|
}
|
|
1217
1380
|
return lineCount;
|
|
1218
1381
|
}
|
|
1219
|
-
async function deployServicesInParallel(
|
|
1220
|
-
|
|
1382
|
+
async function deployServicesInParallel(services) {
|
|
1383
|
+
const client = await getClient();
|
|
1384
|
+
const mode = getOutputMode();
|
|
1385
|
+
if (mode === "json") emitData({
|
|
1386
|
+
type: "parallel_deploy_start",
|
|
1387
|
+
services: services.map((s) => ({
|
|
1388
|
+
serviceId: s.serviceId,
|
|
1389
|
+
serviceName: s.serviceName
|
|
1390
|
+
}))
|
|
1391
|
+
});
|
|
1392
|
+
else if (mode === "github-actions") process.stdout.write(`Iniciando deploy de ${services.length} serviço(s)\n`);
|
|
1393
|
+
else console.log(chalk.cyan(`\nIniciando deploy de ${services.length} serviço(s)...\n`));
|
|
1394
|
+
setupSigintHandler();
|
|
1221
1395
|
const progressMap = /* @__PURE__ */ new Map();
|
|
1222
1396
|
const projectRoot = process.cwd();
|
|
1223
1397
|
const sizeInBytes = await calculateDirectorySize(projectRoot);
|
|
1224
1398
|
const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
|
|
1225
1399
|
const deploymentPromises = services.map(async (service) => {
|
|
1226
1400
|
try {
|
|
1227
|
-
const deployment = await withRetry
|
|
1228
|
-
|
|
1229
|
-
|
|
1401
|
+
const deployment = await withRetry(() => client.deployments.create({ serviceId: service.serviceId }));
|
|
1402
|
+
await withRetry(() => uploadSource(deployment.id, projectRoot, service.extraFiles));
|
|
1403
|
+
trackDeployment(deployment.id);
|
|
1230
1404
|
progressMap.set(service.serviceId, {
|
|
1231
1405
|
serviceName: service.serviceName,
|
|
1232
1406
|
deploymentId: deployment.id,
|
|
@@ -1235,13 +1409,27 @@ async function deployServicesInParallel(client, services) {
|
|
|
1235
1409
|
completed: false,
|
|
1236
1410
|
success: false
|
|
1237
1411
|
});
|
|
1238
|
-
|
|
1412
|
+
if (mode === "json") emitData({
|
|
1413
|
+
type: "upload_complete",
|
|
1414
|
+
serviceId: service.serviceId,
|
|
1415
|
+
serviceName: service.serviceName,
|
|
1416
|
+
deploymentId: deployment.id,
|
|
1417
|
+
sizeMB
|
|
1418
|
+
});
|
|
1419
|
+
else if (mode === "github-actions") process.stdout.write(`✓ ${service.serviceName}: Upload concluído (${sizeMB} MB)\n`);
|
|
1420
|
+
else console.log(`${chalk.green("✓")} ${chalk.bold(service.serviceName)}: Upload concluído ${chalk.dim(`(${sizeMB} MB)`)}`);
|
|
1239
1421
|
return {
|
|
1240
1422
|
service,
|
|
1241
1423
|
deploymentId: deployment.id
|
|
1242
1424
|
};
|
|
1243
1425
|
} catch (error) {
|
|
1244
|
-
|
|
1426
|
+
if (mode === "json") emitData({
|
|
1427
|
+
type: "upload_failed",
|
|
1428
|
+
serviceId: service.serviceId,
|
|
1429
|
+
serviceName: service.serviceName
|
|
1430
|
+
});
|
|
1431
|
+
else if (mode === "github-actions") process.stdout.write(`::error::${service.serviceName}: Falha ao iniciar deploy\n`);
|
|
1432
|
+
else console.log(`${chalk.red("✗")} ${chalk.bold(service.serviceName)}: Falha ao iniciar deploy`);
|
|
1245
1433
|
progressMap.set(service.serviceId, {
|
|
1246
1434
|
serviceName: service.serviceName,
|
|
1247
1435
|
deploymentId: "",
|
|
@@ -1255,18 +1443,28 @@ async function deployServicesInParallel(client, services) {
|
|
|
1255
1443
|
});
|
|
1256
1444
|
const activeDeployments = (await Promise.allSettled(deploymentPromises)).filter((d) => d.status === "fulfilled").map((d) => d.value);
|
|
1257
1445
|
if (activeDeployments.length === 0) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1446
|
+
if (mode === "json") emitData({
|
|
1447
|
+
type: "error",
|
|
1448
|
+
message: "Todos os deploys falharam ao iniciar."
|
|
1449
|
+
});
|
|
1450
|
+
else if (mode === "github-actions") process.stdout.write("::error::Todos os deploys falharam ao iniciar.\n");
|
|
1451
|
+
else console.error(chalk.red("\n✗ Todos os deploys falharam ao iniciar."));
|
|
1452
|
+
process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
if (mode === "json") emitData({
|
|
1455
|
+
type: "monitoring_deploys",
|
|
1456
|
+
count: activeDeployments.length
|
|
1457
|
+
});
|
|
1458
|
+
else if (mode === "github-actions") process.stdout.write(`Monitorando ${activeDeployments.length} deploy(s)\n`);
|
|
1459
|
+
else {
|
|
1460
|
+
console.log(chalk.cyan(`\nMonitorando progresso dos deploys:\n`));
|
|
1461
|
+
console.log(chalk.dim("─".repeat(50)) + "\n");
|
|
1260
1462
|
}
|
|
1261
|
-
console.log(chalk.cyan("\n📦 Monitorando progresso dos deploys:\n"));
|
|
1262
|
-
console.log(chalk.dim("─".repeat(60)) + "\n");
|
|
1263
1463
|
let lineCount = 0;
|
|
1264
|
-
lineCount = renderProgress(progressMap, lineCount);
|
|
1265
|
-
const isVerbose = process.env.VELOZ_VERBOSE === "true";
|
|
1464
|
+
if (mode === "fancy") lineCount = renderProgress(progressMap, lineCount);
|
|
1266
1465
|
const streamPromises = activeDeployments.map(async ({ service, deploymentId }) => {
|
|
1267
1466
|
try {
|
|
1268
1467
|
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
1468
|
const stream = await client.logs.streamBuildLogs({ deploymentId });
|
|
1271
1469
|
for await (const event of stream) {
|
|
1272
1470
|
const progress = progressMap.get(service.serviceId);
|
|
@@ -1277,443 +1475,865 @@ async function deployServicesInParallel(client, services) {
|
|
|
1277
1475
|
progress.completed = true;
|
|
1278
1476
|
progress.success = event.content === "LIVE";
|
|
1279
1477
|
}
|
|
1280
|
-
if (
|
|
1478
|
+
if (mode === "json") emitData({
|
|
1479
|
+
type: "deploy_status",
|
|
1480
|
+
serviceId: service.serviceId,
|
|
1481
|
+
serviceName: service.serviceName,
|
|
1482
|
+
deploymentId,
|
|
1483
|
+
status: event.content
|
|
1484
|
+
});
|
|
1485
|
+
else if (mode === "plain") {
|
|
1486
|
+
const label = statusLabels[event.content] || event.content;
|
|
1487
|
+
process.stdout.write(`[${service.serviceName}] ${label}\n`);
|
|
1488
|
+
}
|
|
1281
1489
|
} else if (event.type === "log") {
|
|
1282
1490
|
const newLines = event.content.split("\n").filter((l) => l.trim());
|
|
1283
1491
|
progress.logLines.push(...newLines);
|
|
1492
|
+
if (mode === "json") emitData({
|
|
1493
|
+
type: "build_log",
|
|
1494
|
+
serviceId: service.serviceId,
|
|
1495
|
+
serviceName: service.serviceName,
|
|
1496
|
+
deploymentId,
|
|
1497
|
+
lines: newLines
|
|
1498
|
+
});
|
|
1499
|
+
else if (mode === "github-actions" || mode === "plain") for (const line of newLines) process.stdout.write(`[${service.serviceName}] ${line}\n`);
|
|
1284
1500
|
}
|
|
1285
|
-
lineCount = renderProgress(progressMap, lineCount);
|
|
1501
|
+
if (mode === "fancy") lineCount = renderProgress(progressMap, lineCount);
|
|
1286
1502
|
}
|
|
1287
1503
|
} catch (error) {
|
|
1288
1504
|
const progress = progressMap.get(service.serviceId);
|
|
1289
1505
|
if (progress && !progress.completed) {
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1506
|
+
if (mode === "json") emitData({
|
|
1507
|
+
type: "deploy_stream_error",
|
|
1508
|
+
serviceId: service.serviceId,
|
|
1509
|
+
serviceName: service.serviceName,
|
|
1510
|
+
error: error instanceof Error ? error.message : "Erro desconhecido"
|
|
1511
|
+
});
|
|
1512
|
+
else if (mode === "github-actions") process.stdout.write(`::error::Erro no streaming de logs para ${service.serviceName}\n`);
|
|
1513
|
+
else if (mode !== "fancy") console.error(`${chalk.red("✗")} Erro no streaming de logs para ${service.serviceName}`);
|
|
1292
1514
|
progress.status = "FAILED";
|
|
1293
1515
|
progress.completed = true;
|
|
1294
|
-
lineCount = renderProgress(progressMap, lineCount);
|
|
1516
|
+
if (mode === "fancy") lineCount = renderProgress(progressMap, lineCount);
|
|
1295
1517
|
}
|
|
1518
|
+
} finally {
|
|
1519
|
+
untrackDeployment(deploymentId);
|
|
1296
1520
|
}
|
|
1297
1521
|
});
|
|
1298
1522
|
await Promise.all(streamPromises);
|
|
1299
|
-
renderProgress(progressMap, lineCount);
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
if (
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1523
|
+
if (mode === "fancy") renderProgress(progressMap, lineCount);
|
|
1524
|
+
const successfulEntries = Array.from(progressMap.entries()).filter(([, p]) => p.success);
|
|
1525
|
+
const failedEntries = Array.from(progressMap.entries()).filter(([, p]) => !p.success);
|
|
1526
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
1527
|
+
await Promise.all(successfulEntries.map(async ([serviceId]) => {
|
|
1528
|
+
const urls = await fetchDeployUrls$1(client, serviceId);
|
|
1529
|
+
if (urls.length > 0) urlMap.set(serviceId, urls);
|
|
1530
|
+
}));
|
|
1531
|
+
if (mode === "json") emitData({
|
|
1532
|
+
type: "parallel_deploy_complete",
|
|
1533
|
+
successful: successfulEntries.map(([serviceId, p]) => ({
|
|
1534
|
+
serviceName: p.serviceName,
|
|
1535
|
+
deploymentId: p.deploymentId,
|
|
1536
|
+
urls: urlMap.get(serviceId) ?? []
|
|
1537
|
+
})),
|
|
1538
|
+
failed: failedEntries.map(([, p]) => ({
|
|
1539
|
+
serviceName: p.serviceName,
|
|
1540
|
+
deploymentId: p.deploymentId,
|
|
1541
|
+
status: p.status,
|
|
1542
|
+
hints: getFailureHints$1(p.status)
|
|
1543
|
+
}))
|
|
1544
|
+
});
|
|
1545
|
+
else {
|
|
1546
|
+
if (mode === "fancy") console.log(chalk.dim("\n" + "─".repeat(50)));
|
|
1547
|
+
if (successfulEntries.length > 0) if (mode === "github-actions") {
|
|
1548
|
+
process.stdout.write(`\n✓ ${successfulEntries.length} serviço(s) implantado(s) com sucesso\n`);
|
|
1549
|
+
for (const [serviceId, progress] of successfulEntries) {
|
|
1550
|
+
process.stdout.write(` ✓ ${progress.serviceName}\n`);
|
|
1551
|
+
for (const url of urlMap.get(serviceId) ?? []) process.stdout.write(` ${url}\n`);
|
|
1552
|
+
}
|
|
1553
|
+
} else if (mode === "plain") {
|
|
1554
|
+
process.stdout.write(`\n${successfulEntries.length} serviço(s) implantado(s) com sucesso:\n`);
|
|
1555
|
+
for (const [serviceId, progress] of successfulEntries) {
|
|
1556
|
+
process.stdout.write(` ✓ ${progress.serviceName}\n`);
|
|
1557
|
+
for (const url of urlMap.get(serviceId) ?? []) process.stdout.write(` ${url}\n`);
|
|
1558
|
+
}
|
|
1559
|
+
} else {
|
|
1560
|
+
console.log(chalk.green(`\n✓ ${successfulEntries.length} serviço(s) implantado(s) com sucesso:\n`));
|
|
1561
|
+
for (const [serviceId, progress] of successfulEntries) {
|
|
1562
|
+
console.log(` ${chalk.green("✓")} ${chalk.bold(progress.serviceName)}`);
|
|
1563
|
+
for (const url of urlMap.get(serviceId) ?? []) console.log(` ${chalk.cyan(url)}`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (failedEntries.length > 0) if (mode === "github-actions") {
|
|
1567
|
+
process.stdout.write(`\n✗ ${failedEntries.length} serviço(s) falhou(aram)\n`);
|
|
1568
|
+
for (const [, progress] of failedEntries) {
|
|
1569
|
+
process.stdout.write(`::error::${progress.serviceName} falhou (${progress.status})\n`);
|
|
1570
|
+
const hints = getFailureHints$1(progress.status);
|
|
1571
|
+
for (const hint of hints) process.stdout.write(` ${hint}\n`);
|
|
1572
|
+
}
|
|
1573
|
+
} else if (mode === "plain") {
|
|
1574
|
+
process.stdout.write(`\n${failedEntries.length} serviço(s) falhou(aram):\n`);
|
|
1575
|
+
for (const [, progress] of failedEntries) {
|
|
1576
|
+
process.stdout.write(`\n ✗ ${progress.serviceName} (${progress.status})\n`);
|
|
1577
|
+
const hints = getFailureHints$1(progress.status);
|
|
1578
|
+
for (const hint of hints) process.stdout.write(` → ${hint}\n`);
|
|
1579
|
+
}
|
|
1580
|
+
} else {
|
|
1581
|
+
console.log(chalk.red(`\n✗ ${failedEntries.length} serviço(s) falhou(aram):\n`));
|
|
1582
|
+
for (const [, progress] of failedEntries) {
|
|
1583
|
+
console.log(` ${chalk.red("✗")} ${chalk.bold(progress.serviceName)} ${chalk.dim(`(${progress.status})`)}`);
|
|
1584
|
+
if (progress.logLines.length > 0) {
|
|
1585
|
+
console.log(chalk.red(` ${"─".repeat(50)}`));
|
|
1586
|
+
console.log(chalk.red.bold(" Logs de build:"));
|
|
1587
|
+
console.log(chalk.red(` ${"─".repeat(50)}`));
|
|
1588
|
+
for (const line of progress.logLines) if (line.trim()) console.log(` ${chalk.dim(line)}`);
|
|
1589
|
+
console.log(chalk.red(` ${"─".repeat(50)}`));
|
|
1590
|
+
}
|
|
1591
|
+
const hints = getFailureHints$1(progress.status);
|
|
1592
|
+
for (const hint of hints) console.log(chalk.yellow(` → ${hint}`));
|
|
1316
1593
|
}
|
|
1317
1594
|
}
|
|
1595
|
+
if (successfulEntries.length > 0) info("\nUse 'veloz logs -f' para acompanhar os logs de execução.");
|
|
1318
1596
|
}
|
|
1319
|
-
if (
|
|
1597
|
+
if (failedEntries.length > 0) process.exit(1);
|
|
1320
1598
|
}
|
|
1321
1599
|
|
|
1322
1600
|
//#endregion
|
|
1323
|
-
//#region src/lib/
|
|
1324
|
-
function
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1601
|
+
//#region src/lib/deploy-stream.ts
|
|
1602
|
+
async function fetchDeployUrls(client, serviceId) {
|
|
1603
|
+
try {
|
|
1604
|
+
return (await client.domains.list({ serviceId })).map((d) => `https://${d.domain}`);
|
|
1605
|
+
} catch {
|
|
1606
|
+
return [];
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function getFailureHints(status) {
|
|
1610
|
+
switch (status) {
|
|
1611
|
+
case "BUILD_FAILED": return [
|
|
1612
|
+
"Verifique os logs de build acima para erros de compilação",
|
|
1613
|
+
"Teste o build localmente: rode o comando de build do seu projeto",
|
|
1614
|
+
"Use 'veloz config show' para verificar as configurações"
|
|
1615
|
+
];
|
|
1616
|
+
case "DEPLOY_FAILED": return [
|
|
1617
|
+
"O build passou mas o serviço falhou ao iniciar",
|
|
1618
|
+
"Verifique se a porta configurada está correta: 'veloz config show'",
|
|
1619
|
+
"Veja os logs de runtime: 'veloz logs -f'"
|
|
1620
|
+
];
|
|
1621
|
+
case "CANCELLED": return ["Deploy cancelado. Execute 'veloz deploy' para tentar novamente."];
|
|
1622
|
+
default: return ["Execute 'veloz logs -f' para mais detalhes."];
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
|
|
1626
|
+
const client = await getClient();
|
|
1627
|
+
const isVerbose = process.env.VELOZ_VERBOSE === "true";
|
|
1628
|
+
const mode = getOutputMode();
|
|
1629
|
+
const allLogLines = [];
|
|
1630
|
+
let buildSpinner = null;
|
|
1631
|
+
if (mode === "json") emitData({
|
|
1632
|
+
type: "deploy_stream_start",
|
|
1633
|
+
deploymentId,
|
|
1634
|
+
serviceName: serviceName ?? null
|
|
1635
|
+
});
|
|
1636
|
+
else if (mode === "github-actions") startGroup(serviceName ? `Build: ${serviceName}` : "Build");
|
|
1637
|
+
else if (mode === "fancy" && !isVerbose) buildSpinner = ora({
|
|
1638
|
+
text: "Aguardando início do build...",
|
|
1639
|
+
color: "cyan"
|
|
1640
|
+
}).start();
|
|
1641
|
+
else {
|
|
1642
|
+
const header = serviceName ? `Build: ${chalk.bold(serviceName)}` : "Build";
|
|
1643
|
+
console.log(chalk.cyan(`\n${header}`));
|
|
1644
|
+
console.log(chalk.dim("─".repeat(50)));
|
|
1330
1645
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1646
|
+
let finalStatus = "";
|
|
1647
|
+
try {
|
|
1648
|
+
const stream = await client.logs.streamBuildLogs({ deploymentId });
|
|
1649
|
+
for await (const event of stream) if (event.type === "status") {
|
|
1650
|
+
const label = statusLabels[event.content] ?? event.content;
|
|
1651
|
+
finalStatus = event.content;
|
|
1652
|
+
if (mode === "json") emitData({
|
|
1653
|
+
type: "deploy_status",
|
|
1654
|
+
deploymentId,
|
|
1655
|
+
status: event.content,
|
|
1656
|
+
label
|
|
1657
|
+
});
|
|
1658
|
+
else if (mode === "fancy" && !isVerbose) {
|
|
1659
|
+
if (buildSpinner) {
|
|
1660
|
+
if (event.content === "BUILDING") buildSpinner.text = "Construindo...";
|
|
1661
|
+
else if (event.content === "DEPLOYING") {
|
|
1662
|
+
buildSpinner.succeed("Build concluído");
|
|
1663
|
+
buildSpinner = ora({
|
|
1664
|
+
text: "Publicando...",
|
|
1665
|
+
color: "cyan"
|
|
1666
|
+
}).start();
|
|
1667
|
+
} else if (event.content === "LIVE") {
|
|
1668
|
+
buildSpinner.succeed("Publicado");
|
|
1669
|
+
buildSpinner = null;
|
|
1670
|
+
} else if (TERMINAL_STATUSES.has(event.content) && event.content !== "LIVE") {
|
|
1671
|
+
buildSpinner.fail(label);
|
|
1672
|
+
buildSpinner = null;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
} else {
|
|
1676
|
+
const icon = statusIcons[event.content] ?? chalk.yellow("●");
|
|
1677
|
+
process.stdout.write(`\n${icon} ${chalk.bold(label)}\n`);
|
|
1678
|
+
}
|
|
1679
|
+
} else if (event.type === "log") {
|
|
1680
|
+
const lines = event.content.split("\n");
|
|
1681
|
+
allLogLines.push(...lines);
|
|
1682
|
+
if (mode === "json") emitData({
|
|
1683
|
+
type: "build_log",
|
|
1684
|
+
deploymentId,
|
|
1685
|
+
content: event.content
|
|
1686
|
+
});
|
|
1687
|
+
else if (mode === "fancy" && !isVerbose) for (const line of lines) {
|
|
1688
|
+
const trimmed = line.trim();
|
|
1689
|
+
if (trimmed) {
|
|
1690
|
+
const display = trimmed.length > 60 ? trimmed.substring(0, 57) + "..." : trimmed;
|
|
1691
|
+
if (buildSpinner) buildSpinner.text = display;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
else for (const line of lines) if (line.trim()) process.stdout.write(` ${line}\n`);
|
|
1695
|
+
}
|
|
1696
|
+
} catch {
|
|
1697
|
+
if (buildSpinner) {
|
|
1698
|
+
buildSpinner.stop();
|
|
1699
|
+
buildSpinner = null;
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
finalStatus = (await client.deployments.get({ deploymentId })).status;
|
|
1703
|
+
try {
|
|
1704
|
+
const logs = await client.logs.getBuildLogs({ deploymentId });
|
|
1705
|
+
if (logs.buildLogs) {
|
|
1706
|
+
allLogLines.push(...logs.buildLogs.split("\n"));
|
|
1707
|
+
if (mode === "json") emitData({
|
|
1708
|
+
type: "build_log",
|
|
1709
|
+
deploymentId,
|
|
1710
|
+
content: logs.buildLogs
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
} catch {}
|
|
1714
|
+
} catch {}
|
|
1715
|
+
}
|
|
1716
|
+
if (mode === "github-actions") endGroup();
|
|
1717
|
+
if (finalStatus === "LIVE") {
|
|
1718
|
+
if (buildSpinner) {
|
|
1719
|
+
buildSpinner.stop();
|
|
1720
|
+
buildSpinner = null;
|
|
1721
|
+
}
|
|
1722
|
+
success(serviceName ? `Deploy de ${chalk.bold(serviceName)} concluído! Serviço ativo.` : "Deploy concluído! Serviço ativo.");
|
|
1723
|
+
const urls = await fetchDeployUrls(client, serviceId);
|
|
1724
|
+
if (mode === "json") emitData({
|
|
1725
|
+
type: "deploy_complete",
|
|
1726
|
+
deploymentId,
|
|
1727
|
+
status: "LIVE",
|
|
1728
|
+
serviceName: serviceName ?? null,
|
|
1729
|
+
urls
|
|
1730
|
+
});
|
|
1731
|
+
else if (urls.length > 0) for (const url of urls) info(`${chalk.bold(url)}`);
|
|
1732
|
+
} else if (TERMINAL_STATUSES.has(finalStatus)) {
|
|
1733
|
+
if (buildSpinner) {
|
|
1734
|
+
buildSpinner.stop();
|
|
1735
|
+
buildSpinner = null;
|
|
1736
|
+
}
|
|
1737
|
+
const label = statusLabels[finalStatus] ?? finalStatus;
|
|
1738
|
+
const hints = getFailureHints(finalStatus);
|
|
1739
|
+
if (mode === "fancy" && allLogLines.length > 0) {
|
|
1740
|
+
console.log();
|
|
1741
|
+
console.log(chalk.red(` ${"─".repeat(50)}`));
|
|
1742
|
+
console.log(chalk.red.bold(" Logs de build:"));
|
|
1743
|
+
console.log(chalk.red(` ${"─".repeat(50)}`));
|
|
1744
|
+
for (const line of allLogLines) if (line.trim()) console.log(chalk.dim(` ${line}`));
|
|
1745
|
+
console.log(chalk.red(` ${"─".repeat(50)}`));
|
|
1746
|
+
}
|
|
1747
|
+
if (mode === "json") emitData({
|
|
1748
|
+
type: "deploy_complete",
|
|
1749
|
+
deploymentId,
|
|
1750
|
+
status: finalStatus,
|
|
1751
|
+
serviceName: serviceName ?? null,
|
|
1752
|
+
hints
|
|
1753
|
+
});
|
|
1754
|
+
else if (mode === "github-actions") {
|
|
1755
|
+
const msg = serviceName ? `Deploy de ${serviceName} finalizou com status: ${label}` : `Deploy finalizou com status: ${label}`;
|
|
1756
|
+
process.stdout.write(`::error::${msg}\n`);
|
|
1757
|
+
for (const hint of hints) process.stdout.write(` ${hint}\n`);
|
|
1758
|
+
} else {
|
|
1759
|
+
const errorMsg = serviceName ? `Deploy de ${chalk.bold(serviceName)} finalizou: ${label}` : `Deploy finalizou: ${label}`;
|
|
1760
|
+
console.error(chalk.red(`\n✗ ${errorMsg}`));
|
|
1761
|
+
for (const hint of hints) console.error(chalk.yellow(` → ${hint}`));
|
|
1762
|
+
}
|
|
1763
|
+
process.exit(1);
|
|
1338
1764
|
}
|
|
1339
1765
|
}
|
|
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
1766
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/lib/brand.ts
|
|
1769
|
+
const LOGO_LINES = [
|
|
1770
|
+
"╦ ╦╔═╗╦ ╔═╗╔═╗",
|
|
1771
|
+
"╚╗╔╝║╣ ║ ║ ║ ╔╝",
|
|
1772
|
+
" ╚╝ ╚═╝╩═╝╚═╝╚═╝"
|
|
1773
|
+
];
|
|
1774
|
+
const BRAND_COLOR = "#FF4D00";
|
|
1775
|
+
function getVersion() {
|
|
1776
|
+
return "0.0.0-beta.10";
|
|
1777
|
+
}
|
|
1778
|
+
function printBanner(subtitle) {
|
|
1779
|
+
const mode = getOutputMode();
|
|
1780
|
+
const version = getVersion();
|
|
1781
|
+
const env = getActiveEnv();
|
|
1782
|
+
const envTag = env ? ` [${env}]` : "";
|
|
1783
|
+
if (mode === "json") return;
|
|
1784
|
+
if (mode === "github-actions") {
|
|
1785
|
+
process.stdout.write(`Veloz ${subtitle ?? ""} v${version}${envTag}\n`);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (mode === "plain") {
|
|
1789
|
+
process.stdout.write(`\nVeloz ${subtitle ?? ""} v${version}${envTag}\n\n`);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const sub = subtitle ?? "";
|
|
1793
|
+
const envBadge = env ? ` ${chalk.bgYellow.black(` ${env} `)}` : "";
|
|
1794
|
+
console.log();
|
|
1795
|
+
console.log(` ${chalk.hex(BRAND_COLOR).bold(LOGO_LINES[0])}`);
|
|
1796
|
+
console.log(` ${chalk.hex(BRAND_COLOR).bold(LOGO_LINES[1])} ${chalk.bold(sub)}${envBadge}`);
|
|
1797
|
+
console.log(` ${chalk.hex(BRAND_COLOR).bold(LOGO_LINES[2])} ${chalk.dim(`v${version}`)}`);
|
|
1798
|
+
console.log();
|
|
1799
|
+
console.log(` ${chalk.dim("─".repeat(40))}`);
|
|
1800
|
+
console.log();
|
|
1801
|
+
}
|
|
1463
1802
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1803
|
+
//#endregion
|
|
1804
|
+
//#region src/lib/deploy-config.ts
|
|
1805
|
+
function resolveServiceConf(velozConfig, serviceId) {
|
|
1806
|
+
if (!velozConfig) return void 0;
|
|
1807
|
+
for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === serviceId) {
|
|
1808
|
+
const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
|
|
1809
|
+
return {
|
|
1810
|
+
type: merged.type,
|
|
1811
|
+
buildCommand: merged.build?.command ?? void 0,
|
|
1812
|
+
startCommand: merged.runtime?.command ?? void 0,
|
|
1813
|
+
port: merged.runtime?.port ?? void 0,
|
|
1814
|
+
rootDirectory: merged.root,
|
|
1815
|
+
outputDir: merged.build?.outputDir ?? void 0,
|
|
1816
|
+
instanceCount: merged.resources?.instances ?? void 0,
|
|
1817
|
+
cpuLimit: merged.resources?.cpu ?? void 0,
|
|
1818
|
+
memoryLimit: merged.resources?.memory ?? void 0,
|
|
1819
|
+
healthCheckPath: merged.runtime?.healthCheck?.path ?? null,
|
|
1820
|
+
aptPackages: merged.build?.aptPackages ?? void 0,
|
|
1821
|
+
nodeVersion: merged.build?.nodeVersion ?? void 0,
|
|
1822
|
+
nixpkgsArchive: merged.build?.nixpkgsArchive ?? void 0,
|
|
1823
|
+
packageManager: merged.build?.packageManager ?? void 0,
|
|
1824
|
+
installCommand: merged.build?.installCommand ?? void 0
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
async function syncServiceConfig(client, serviceId, conf) {
|
|
1829
|
+
await withRetry(() => client.services.update({
|
|
1830
|
+
serviceId,
|
|
1831
|
+
type: conf.type?.toUpperCase(),
|
|
1832
|
+
port: conf.port,
|
|
1833
|
+
instanceCount: conf.instanceCount,
|
|
1834
|
+
cpuLimit: conf.cpuLimit,
|
|
1835
|
+
memoryLimit: conf.memoryLimit,
|
|
1836
|
+
buildCommand: conf.buildCommand,
|
|
1837
|
+
startCommand: conf.startCommand,
|
|
1838
|
+
rootDirectory: conf.rootDirectory,
|
|
1839
|
+
healthCheckPath: conf.healthCheckPath,
|
|
1840
|
+
aptPackages: conf.aptPackages,
|
|
1841
|
+
nodeVersion: conf.nodeVersion,
|
|
1842
|
+
nixpkgsArchive: conf.nixpkgsArchive,
|
|
1843
|
+
packageManager: conf.packageManager,
|
|
1844
|
+
installCommand: conf.installCommand
|
|
1845
|
+
}));
|
|
1846
|
+
}
|
|
1466
1847
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1848
|
+
//#endregion
|
|
1849
|
+
//#region src/lib/deploy-checks.ts
|
|
1850
|
+
/**
|
|
1851
|
+
* Platform-specific presets that won't work on Veloz (generic K8s).
|
|
1852
|
+
* Maps preset name to the platform it targets.
|
|
1853
|
+
*/
|
|
1854
|
+
const INCOMPATIBLE_PRESETS = {
|
|
1855
|
+
vercel: "Vercel",
|
|
1856
|
+
"cloudflare-pages": "Cloudflare Pages",
|
|
1857
|
+
"cloudflare-workers": "Cloudflare Workers",
|
|
1858
|
+
"cloudflare-module": "Cloudflare Workers",
|
|
1859
|
+
netlify: "Netlify",
|
|
1860
|
+
"netlify-edge": "Netlify Edge",
|
|
1861
|
+
"aws-lambda": "AWS Lambda",
|
|
1862
|
+
firebase: "Firebase",
|
|
1863
|
+
"deno-deploy": "Deno Deploy",
|
|
1864
|
+
"render-com": "Render",
|
|
1865
|
+
"flight-control": "Flightcontrol",
|
|
1866
|
+
stormkit: "Stormkit",
|
|
1867
|
+
edgio: "Edgio",
|
|
1868
|
+
lagon: "Lagon"
|
|
1869
|
+
};
|
|
1870
|
+
/**
|
|
1871
|
+
* Recommended preset based on package manager / runtime.
|
|
1872
|
+
*/
|
|
1873
|
+
function recommendedPreset(basePath) {
|
|
1874
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
1875
|
+
if (!existsSync(pkgPath)) return "node-server";
|
|
1876
|
+
try {
|
|
1877
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1878
|
+
if ("bun" in {
|
|
1879
|
+
...pkg.dependencies,
|
|
1880
|
+
...pkg.devDependencies
|
|
1881
|
+
} || existsSync(resolve(basePath, "bun.lockb")) || existsSync(resolve(basePath, "bun.lock"))) return "bun";
|
|
1882
|
+
} catch {}
|
|
1883
|
+
return "node-server";
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Check for Nitro preset misconfigurations in vite/nuxt config files.
|
|
1887
|
+
*/
|
|
1888
|
+
function checkNitroPreset(basePath) {
|
|
1889
|
+
for (const file of [
|
|
1890
|
+
"vite.config.ts",
|
|
1891
|
+
"vite.config.js",
|
|
1892
|
+
"vite.config.mjs",
|
|
1893
|
+
"nuxt.config.ts",
|
|
1894
|
+
"nuxt.config.js"
|
|
1895
|
+
]) {
|
|
1896
|
+
const filePath = resolve(basePath, file);
|
|
1897
|
+
if (!existsSync(filePath)) continue;
|
|
1898
|
+
let content;
|
|
1899
|
+
try {
|
|
1900
|
+
content = readFileSync(filePath, "utf-8");
|
|
1901
|
+
} catch {
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
const presetMatch = content.match(/preset\s*:\s*["']([^"']+)["']/);
|
|
1905
|
+
if (!presetMatch) continue;
|
|
1906
|
+
const preset = presetMatch[1];
|
|
1907
|
+
const platform$1 = INCOMPATIBLE_PRESETS[preset];
|
|
1908
|
+
if (!platform$1) continue;
|
|
1909
|
+
const recommended = recommendedPreset(basePath);
|
|
1910
|
+
return {
|
|
1911
|
+
message: `${file} usa preset "${preset}" (${platform$1}) — incompativel com Veloz`,
|
|
1912
|
+
hint: `Altere para preset: "${recommended}" em ${file}`
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Check for Dockerfile COPY instructions that reference both bun.lockb and bun.lock.
|
|
1919
|
+
* Only one usually exists — the COPY will fail if both are listed but one is missing.
|
|
1920
|
+
*/
|
|
1921
|
+
function checkDockerfileLockFiles(basePath) {
|
|
1922
|
+
const dockerfilePath = resolve(basePath, "Dockerfile");
|
|
1923
|
+
if (!existsSync(dockerfilePath)) return null;
|
|
1924
|
+
let content;
|
|
1925
|
+
try {
|
|
1926
|
+
content = readFileSync(dockerfilePath, "utf-8");
|
|
1927
|
+
} catch {
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
const copyLines = content.split("\n").filter((l) => /^COPY\s/.test(l.trim()));
|
|
1931
|
+
for (const line of copyLines) if (line.includes("bun.lockb") && line.includes("bun.lock") && !line.includes("bun.lock*")) {
|
|
1932
|
+
if (!(existsSync(resolve(basePath, "bun.lockb")) && existsSync(resolve(basePath, "bun.lock")))) return {
|
|
1933
|
+
message: "Dockerfile lista bun.lockb e bun.lock mas apenas um existe",
|
|
1934
|
+
hint: "Use \"COPY package.json bun.lock* ./\" para copiar o que existir"
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Next.js needs `output: "standalone"` for Docker/K8s deploys.
|
|
1941
|
+
* Without it, the build produces a node_modules-dependent output
|
|
1942
|
+
* that's huge and doesn't run well in containers.
|
|
1943
|
+
*/
|
|
1944
|
+
function checkNextStandalone(basePath) {
|
|
1945
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
1946
|
+
if (!existsSync(pkgPath)) return null;
|
|
1947
|
+
try {
|
|
1948
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1949
|
+
if (!("next" in {
|
|
1950
|
+
...pkg.dependencies,
|
|
1951
|
+
...pkg.devDependencies
|
|
1952
|
+
})) return null;
|
|
1953
|
+
} catch {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
if (existsSync(resolve(basePath, "Dockerfile"))) return null;
|
|
1957
|
+
for (const file of [
|
|
1958
|
+
"next.config.ts",
|
|
1959
|
+
"next.config.js",
|
|
1960
|
+
"next.config.mjs"
|
|
1961
|
+
]) {
|
|
1962
|
+
const filePath = resolve(basePath, file);
|
|
1963
|
+
if (!existsSync(filePath)) continue;
|
|
1964
|
+
try {
|
|
1965
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1966
|
+
if (content.includes("\"standalone\"") || content.includes("'standalone'")) return null;
|
|
1967
|
+
return {
|
|
1968
|
+
message: `${file} nao tem output: "standalone"`,
|
|
1969
|
+
hint: "Adicione output: \"standalone\" no next.config para deploy na Veloz"
|
|
1970
|
+
};
|
|
1971
|
+
} catch {
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Prisma needs `prisma generate` in the build step.
|
|
1979
|
+
* Without it, the Prisma client won't be generated and the app will crash.
|
|
1980
|
+
*/
|
|
1981
|
+
function checkPrismaGenerate(basePath) {
|
|
1982
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
1983
|
+
if (!existsSync(pkgPath)) return null;
|
|
1984
|
+
try {
|
|
1985
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
1986
|
+
const allDeps = {
|
|
1987
|
+
...pkg.dependencies,
|
|
1988
|
+
...pkg.devDependencies
|
|
1989
|
+
};
|
|
1990
|
+
if (!("prisma" in allDeps) && !("@prisma/client" in allDeps)) return null;
|
|
1991
|
+
const scripts = pkg.scripts || {};
|
|
1992
|
+
const buildScript = scripts.build || "";
|
|
1993
|
+
const postinstall = scripts.postinstall || "";
|
|
1994
|
+
if (buildScript.includes("prisma generate") || postinstall.includes("prisma generate") || buildScript.includes("prisma db push")) return null;
|
|
1995
|
+
return {
|
|
1996
|
+
message: "Prisma detectado mas prisma generate nao esta no build/postinstall",
|
|
1997
|
+
hint: "Adicione \"prisma generate\" ao script postinstall ou build no package.json"
|
|
1998
|
+
};
|
|
1999
|
+
} catch {
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Detect if the app hardcodes a port that doesn't match the configured port.
|
|
2005
|
+
* Common issue: app listens on 8080 but service port is 3000.
|
|
2006
|
+
*/
|
|
2007
|
+
function checkPortMismatch(basePath) {
|
|
2008
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
2009
|
+
if (!existsSync(pkgPath)) return null;
|
|
2010
|
+
try {
|
|
2011
|
+
const portMatch = ((JSON.parse(readFileSync(pkgPath, "utf-8")).scripts || {}).start || "").match(/(?:--port|-p)\s+(\d+)/);
|
|
2012
|
+
if (portMatch) {
|
|
2013
|
+
const hardcodedPort = parseInt(portMatch[1], 10);
|
|
2014
|
+
if (hardcodedPort !== 3e3) return {
|
|
2015
|
+
message: `Script start usa porta ${hardcodedPort} — certifique-se de que a porta do servico esta configurada corretamente`,
|
|
2016
|
+
hint: `Configure a porta do servico para ${hardcodedPort} no dashboard ou veloz.json`
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
} catch {}
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Check for .env files that might be accidentally uploaded.
|
|
2024
|
+
*/
|
|
2025
|
+
function checkEnvFileCommitted(basePath) {
|
|
2026
|
+
if (!existsSync(resolve(basePath, ".env"))) return null;
|
|
2027
|
+
const gitignorePath = resolve(basePath, ".gitignore");
|
|
2028
|
+
if (existsSync(gitignorePath)) try {
|
|
2029
|
+
const lines = readFileSync(gitignorePath, "utf-8").split("\n").map((l) => l.trim());
|
|
2030
|
+
if (lines.includes(".env") || lines.includes(".env*") || lines.includes("*.env")) return null;
|
|
2031
|
+
} catch {}
|
|
2032
|
+
return {
|
|
2033
|
+
message: "Arquivo .env encontrado e nao esta no .gitignore",
|
|
2034
|
+
hint: "Adicione .env ao .gitignore — use variaveis de ambiente no dashboard ou CLI"
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Node.js project without a start command — nixpacks won't know how to run it.
|
|
2039
|
+
* Checks for: scripts.start, main field, or common entry files.
|
|
2040
|
+
*/
|
|
2041
|
+
function checkMissingStartCommand(basePath) {
|
|
2042
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
2043
|
+
if (!existsSync(pkgPath)) return null;
|
|
2044
|
+
if (existsSync(resolve(basePath, "Dockerfile"))) return null;
|
|
2045
|
+
try {
|
|
2046
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2047
|
+
if (pkg.scripts?.start) return null;
|
|
2048
|
+
if (pkg.main) return null;
|
|
2049
|
+
const allDeps = {
|
|
2050
|
+
...pkg.dependencies,
|
|
2051
|
+
...pkg.devDependencies
|
|
2052
|
+
};
|
|
2053
|
+
if ([
|
|
2054
|
+
"next",
|
|
2055
|
+
"nuxt",
|
|
2056
|
+
"nuxt3",
|
|
2057
|
+
"@sveltejs/kit",
|
|
2058
|
+
"remix",
|
|
2059
|
+
"astro",
|
|
2060
|
+
"@angular/core",
|
|
2061
|
+
"gatsby"
|
|
2062
|
+
].some((f) => f in allDeps)) return null;
|
|
2063
|
+
if ([
|
|
2064
|
+
"index.js",
|
|
2065
|
+
"index.mjs",
|
|
2066
|
+
"index.ts",
|
|
2067
|
+
"server.js",
|
|
2068
|
+
"server.ts",
|
|
2069
|
+
"app.js",
|
|
2070
|
+
"app.ts",
|
|
2071
|
+
"src/index.js",
|
|
2072
|
+
"src/index.ts",
|
|
2073
|
+
"src/server.js",
|
|
2074
|
+
"src/server.ts"
|
|
2075
|
+
].some((f) => existsSync(resolve(basePath, f)))) return null;
|
|
2076
|
+
return {
|
|
2077
|
+
message: "Nenhum script start encontrado no package.json",
|
|
2078
|
+
hint: "Adicione \"start\" em scripts (ex: \"node dist/index.js\") ou um campo \"main\" no package.json"
|
|
2079
|
+
};
|
|
2080
|
+
} catch {
|
|
2081
|
+
return null;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* packageManager field in package.json doesn't match the lockfile present.
|
|
2086
|
+
* e.g., packageManager: "pnpm@9.0.0" but only package-lock.json exists.
|
|
2087
|
+
*/
|
|
2088
|
+
function checkPackageManagerMismatch(basePath) {
|
|
2089
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
2090
|
+
if (!existsSync(pkgPath)) return null;
|
|
2091
|
+
try {
|
|
2092
|
+
const pmField = JSON.parse(readFileSync(pkgPath, "utf-8")).packageManager;
|
|
2093
|
+
if (!pmField) return null;
|
|
2094
|
+
const declaredPm = pmField.split("@")[0];
|
|
2095
|
+
const lockfileMap = {
|
|
2096
|
+
npm: ["package-lock.json"],
|
|
2097
|
+
yarn: ["yarn.lock"],
|
|
2098
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
2099
|
+
bun: ["bun.lockb", "bun.lock"]
|
|
2100
|
+
};
|
|
2101
|
+
const expectedLockfiles = lockfileMap[declaredPm];
|
|
2102
|
+
if (!expectedLockfiles) return null;
|
|
2103
|
+
if (expectedLockfiles.some((f) => existsSync(resolve(basePath, f)))) return null;
|
|
2104
|
+
const otherPms = Object.entries(lockfileMap).filter(([pm]) => pm !== declaredPm);
|
|
2105
|
+
for (const [pm, files] of otherPms) if (files.some((f) => existsSync(resolve(basePath, f)))) return {
|
|
2106
|
+
message: `packageManager "${pmField}" no package.json mas lockfile de ${pm} encontrado`,
|
|
2107
|
+
hint: `Remova o campo packageManager ou gere o lockfile correto com ${declaredPm} install`
|
|
2108
|
+
};
|
|
2109
|
+
} catch {}
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Native modules that need system packages to build.
|
|
2114
|
+
* The server auto-detects and injects apt packages for known modules (sharp, canvas,
|
|
2115
|
+
* puppeteer, playwright-chromium). This check warns about bcrypt which has a pure-JS
|
|
2116
|
+
* alternative, and modules not in the auto-detect list.
|
|
2117
|
+
*/
|
|
2118
|
+
function checkNativeModules(basePath) {
|
|
2119
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
2120
|
+
if (!existsSync(pkgPath)) return null;
|
|
2121
|
+
if (existsSync(resolve(basePath, "Dockerfile"))) return null;
|
|
2122
|
+
try {
|
|
2123
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2124
|
+
if ("bcrypt" in {
|
|
2125
|
+
...pkg.dependencies,
|
|
2126
|
+
...pkg.devDependencies
|
|
2127
|
+
}) return {
|
|
2128
|
+
message: "bcrypt compila codigo nativo — pode falhar em alguns ambientes",
|
|
2129
|
+
hint: "Considere usar bcryptjs (pure JS) para evitar falhas de build"
|
|
2130
|
+
};
|
|
2131
|
+
} catch {}
|
|
2132
|
+
return null;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* nixpacks.toml phases that override defaults instead of extending them.
|
|
2136
|
+
* Missing "..." in [phases.X.cmds] replaces all default commands.
|
|
2137
|
+
*/
|
|
2138
|
+
function checkNixpacksTomlSpread(basePath) {
|
|
2139
|
+
const tomlPath = resolve(basePath, "nixpacks.toml");
|
|
2140
|
+
if (!existsSync(tomlPath)) return null;
|
|
2141
|
+
try {
|
|
2142
|
+
const content = readFileSync(tomlPath, "utf-8");
|
|
2143
|
+
if (!content.match(/\[phases\.\w+\]/g)) return null;
|
|
2144
|
+
const hasCmds = /cmds\s*=\s*\[/.test(content);
|
|
2145
|
+
const hasSpread = content.includes("\"...\"");
|
|
2146
|
+
if (hasCmds && !hasSpread) return {
|
|
2147
|
+
message: "nixpacks.toml define cmds sem \"...\" — isso substitui os comandos padrao",
|
|
2148
|
+
hint: "Adicione \"...\" no array cmds para manter os comandos padrao: cmds = [\"...\", \"seu-comando\"]"
|
|
2149
|
+
};
|
|
2150
|
+
} catch {}
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Django project without gunicorn — the dev server isn't suitable for production.
|
|
2155
|
+
*/
|
|
2156
|
+
function checkDjangoGunicorn(basePath) {
|
|
2157
|
+
if (existsSync(resolve(basePath, "Dockerfile"))) return null;
|
|
2158
|
+
const requirementsFiles = [
|
|
2159
|
+
"requirements.txt",
|
|
2160
|
+
"requirements/production.txt",
|
|
2161
|
+
"requirements/prod.txt"
|
|
2162
|
+
];
|
|
2163
|
+
let hasDjango = false;
|
|
2164
|
+
let hasGunicorn = false;
|
|
2165
|
+
for (const file of requirementsFiles) {
|
|
2166
|
+
const filePath = resolve(basePath, file);
|
|
2167
|
+
if (!existsSync(filePath)) continue;
|
|
2168
|
+
try {
|
|
2169
|
+
const content = readFileSync(filePath, "utf-8").toLowerCase();
|
|
2170
|
+
if (content.includes("django")) hasDjango = true;
|
|
2171
|
+
if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
|
|
2172
|
+
} catch {
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
for (const file of ["pyproject.toml", "Pipfile"]) {
|
|
2177
|
+
const filePath = resolve(basePath, file);
|
|
2178
|
+
if (!existsSync(filePath)) continue;
|
|
2179
|
+
try {
|
|
2180
|
+
const content = readFileSync(filePath, "utf-8").toLowerCase();
|
|
2181
|
+
if (content.includes("django")) hasDjango = true;
|
|
2182
|
+
if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
|
|
2183
|
+
} catch {
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (hasDjango && !hasGunicorn) return {
|
|
2188
|
+
message: "Django detectado sem gunicorn/uvicorn — o servidor de dev nao deve ser usado em producao",
|
|
2189
|
+
hint: "Adicione gunicorn ao requirements.txt e configure o start command: \"gunicorn myproject.wsgi\""
|
|
2190
|
+
};
|
|
2191
|
+
return null;
|
|
1469
2192
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
2193
|
+
/**
|
|
2194
|
+
* SvelteKit needs adapter-node for container deploys.
|
|
2195
|
+
* Default adapter-auto or adapter-vercel/netlify won't work.
|
|
2196
|
+
*/
|
|
2197
|
+
function checkSvelteKitAdapter(basePath) {
|
|
2198
|
+
const pkgPath = resolve(basePath, "package.json");
|
|
2199
|
+
if (!existsSync(pkgPath)) return null;
|
|
2200
|
+
try {
|
|
2201
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2202
|
+
const allDeps = {
|
|
2203
|
+
...pkg.dependencies,
|
|
2204
|
+
...pkg.devDependencies
|
|
2205
|
+
};
|
|
2206
|
+
if (!("@sveltejs/kit" in allDeps)) return null;
|
|
2207
|
+
const hasNodeAdapter = "@sveltejs/adapter-node" in allDeps;
|
|
2208
|
+
const hasBunAdapter = "svelte-adapter-bun" in allDeps;
|
|
2209
|
+
if (hasNodeAdapter || hasBunAdapter) return null;
|
|
2210
|
+
const installed = [
|
|
2211
|
+
"@sveltejs/adapter-vercel",
|
|
2212
|
+
"@sveltejs/adapter-netlify",
|
|
2213
|
+
"@sveltejs/adapter-cloudflare",
|
|
2214
|
+
"@sveltejs/adapter-cloudflare-workers"
|
|
2215
|
+
].find((a) => a in allDeps);
|
|
2216
|
+
if (installed) return {
|
|
2217
|
+
message: `SvelteKit usa ${installed} — incompativel com Veloz`,
|
|
2218
|
+
hint: "Instale @sveltejs/adapter-node e configure em svelte.config.js"
|
|
2219
|
+
};
|
|
2220
|
+
if ("@sveltejs/adapter-auto" in allDeps) return {
|
|
2221
|
+
message: "SvelteKit usa adapter-auto — pode nao funcionar no deploy",
|
|
2222
|
+
hint: "Instale @sveltejs/adapter-node para deploys na Veloz"
|
|
2223
|
+
};
|
|
2224
|
+
} catch {}
|
|
2225
|
+
return null;
|
|
1473
2226
|
}
|
|
1474
|
-
|
|
1475
|
-
//#endregion
|
|
1476
|
-
//#region src/lib/templates.ts
|
|
1477
2227
|
/**
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
* then cleaned up immediately after.
|
|
2228
|
+
* Run all pre-deploy checks and return warnings.
|
|
2229
|
+
* Does not block — just warns the user.
|
|
1481
2230
|
*/
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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");
|
|
2231
|
+
function runPreDeployChecks(basePath = ".") {
|
|
2232
|
+
const warnings = [];
|
|
2233
|
+
const fullPath = resolve(process.cwd(), basePath);
|
|
2234
|
+
const checks = [
|
|
2235
|
+
checkNitroPreset,
|
|
2236
|
+
checkDockerfileLockFiles,
|
|
2237
|
+
checkNextStandalone,
|
|
2238
|
+
checkPrismaGenerate,
|
|
2239
|
+
checkPortMismatch,
|
|
2240
|
+
checkEnvFileCommitted,
|
|
2241
|
+
checkSvelteKitAdapter,
|
|
2242
|
+
checkMissingStartCommand,
|
|
2243
|
+
checkPackageManagerMismatch,
|
|
2244
|
+
checkNativeModules,
|
|
2245
|
+
checkNixpacksTomlSpread,
|
|
2246
|
+
checkDjangoGunicorn
|
|
2247
|
+
];
|
|
2248
|
+
for (const check of checks) {
|
|
2249
|
+
const result = check(fullPath);
|
|
2250
|
+
if (result) warnings.push(result);
|
|
2251
|
+
}
|
|
2252
|
+
return warnings;
|
|
1629
2253
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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");
|
|
2254
|
+
/**
|
|
2255
|
+
* Print deploy warnings to the console.
|
|
2256
|
+
* Returns true if any warnings were found.
|
|
2257
|
+
*/
|
|
2258
|
+
function printDeployWarnings(warnings) {
|
|
2259
|
+
if (warnings.length === 0) return false;
|
|
2260
|
+
const mode = getOutputMode();
|
|
2261
|
+
if (mode === "json") {
|
|
2262
|
+
emitData({
|
|
2263
|
+
type: "deploy_warnings",
|
|
2264
|
+
warnings: warnings.map((w) => ({
|
|
2265
|
+
message: w.message,
|
|
2266
|
+
hint: w.hint
|
|
2267
|
+
}))
|
|
2268
|
+
});
|
|
2269
|
+
return true;
|
|
2270
|
+
}
|
|
2271
|
+
console.log();
|
|
2272
|
+
for (const w of warnings) if (mode === "github-actions") process.stdout.write(`::warning::${w.message} — ${w.hint}\n`);
|
|
2273
|
+
else {
|
|
2274
|
+
console.log(chalk.yellow(` AVISO: ${w.message}`));
|
|
2275
|
+
console.log(chalk.dim(` ${w.hint}`));
|
|
2276
|
+
}
|
|
2277
|
+
console.log();
|
|
2278
|
+
return true;
|
|
1673
2279
|
}
|
|
1674
2280
|
|
|
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
2281
|
//#endregion
|
|
1682
|
-
//#region src/
|
|
1683
|
-
|
|
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
|
-
}
|
|
2282
|
+
//#region src/lib/auto-update.ts
|
|
2283
|
+
const PACKAGE_NAME = "onveloz";
|
|
1697
2284
|
function detectPackageManager() {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
2285
|
+
try {
|
|
2286
|
+
const binPath = realpathSync(process.argv[1]);
|
|
2287
|
+
if (binPath.includes("/hostcloud/") || binPath.includes("/onveloz/src/")) return null;
|
|
2288
|
+
if (binPath.includes("/.pnpm/") || binPath.includes("/pnpm/")) return "pnpm";
|
|
2289
|
+
if (binPath.includes("/.bun/")) return "bun";
|
|
2290
|
+
if (binPath.includes("/yarn/")) return "yarn";
|
|
2291
|
+
} catch {}
|
|
1701
2292
|
return "npm";
|
|
1702
2293
|
}
|
|
1703
|
-
function
|
|
2294
|
+
function getInstallCommand(pm, version) {
|
|
2295
|
+
switch (pm) {
|
|
2296
|
+
case "pnpm": return `pnpm install -g ${PACKAGE_NAME}@${version}`;
|
|
2297
|
+
case "yarn": return `yarn global add ${PACKAGE_NAME}@${version}`;
|
|
2298
|
+
case "bun": return `bun install -g ${PACKAGE_NAME}@${version}`;
|
|
2299
|
+
case "npm": return `npm install -g ${PACKAGE_NAME}@${version}`;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
async function fetchLatestVersion() {
|
|
1704
2303
|
try {
|
|
1705
|
-
const
|
|
1706
|
-
if (
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
return
|
|
2304
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
|
|
2305
|
+
if (!res.ok) return null;
|
|
2306
|
+
return (await res.json()).version ?? null;
|
|
2307
|
+
} catch {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
async function autoUpdate() {
|
|
2312
|
+
const pm = detectPackageManager();
|
|
2313
|
+
if (!pm) return;
|
|
2314
|
+
const currentVersion = "0.0.0-beta.10";
|
|
2315
|
+
const latestVersion = await fetchLatestVersion();
|
|
2316
|
+
if (!latestVersion || latestVersion === currentVersion) return;
|
|
2317
|
+
const installCmd = getInstallCommand(pm, latestVersion);
|
|
2318
|
+
const spin = spinner(`Atualizando CLI ${chalk.dim(currentVersion)} → ${chalk.bold(latestVersion)}...`);
|
|
2319
|
+
try {
|
|
2320
|
+
execSync(installCmd, { stdio: "ignore" });
|
|
2321
|
+
spin.stop();
|
|
2322
|
+
console.log(chalk.green(`\n✓ CLI atualizada: ${chalk.bold(latestVersion)}\n`));
|
|
2323
|
+
} catch {
|
|
2324
|
+
spin.stop();
|
|
2325
|
+
console.log(chalk.yellow(`\n⚠ Não foi possível atualizar automaticamente. Execute manualmente:\n ${installCmd}\n`));
|
|
2326
|
+
}
|
|
1715
2327
|
}
|
|
1716
|
-
|
|
2328
|
+
|
|
2329
|
+
//#endregion
|
|
2330
|
+
//#region src/commands/deploy.ts
|
|
2331
|
+
/**
|
|
2332
|
+
* If a Dockerfile exists in a subdirectory (rootDirectory), copy it to tar root
|
|
2333
|
+
* so BuildKit can find it. If no Dockerfile exists anywhere, return nothing —
|
|
2334
|
+
* the server will generate one with nixpacks.
|
|
2335
|
+
*/
|
|
2336
|
+
function prepareExtraFiles(_detection, serviceConfig) {
|
|
1717
2337
|
if (existsSync(resolve(process.cwd(), "Dockerfile"))) return [];
|
|
1718
2338
|
const rootDir = serviceConfig?.rootDirectory || ".";
|
|
1719
2339
|
const serviceDockerfilePath = resolve(process.cwd(), rootDir, "Dockerfile");
|
|
@@ -1721,89 +2341,71 @@ function prepareExtraFiles(detection, serviceConfig) {
|
|
|
1721
2341
|
name: "Dockerfile",
|
|
1722
2342
|
content: readFileSync(serviceDockerfilePath, "utf-8")
|
|
1723
2343
|
}];
|
|
1724
|
-
|
|
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;
|
|
2344
|
+
return [];
|
|
1749
2345
|
}
|
|
1750
|
-
function computeExtraFilesForServices(services) {
|
|
2346
|
+
async function computeExtraFilesForServices(services) {
|
|
1751
2347
|
const velozConfig = loadConfig();
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2348
|
+
const client = await getClient();
|
|
2349
|
+
const results = [];
|
|
2350
|
+
const allWarnings = [];
|
|
2351
|
+
for (const svc of services) {
|
|
2352
|
+
const warnings = runPreDeployChecks(resolveServiceConf(velozConfig, svc.serviceId)?.rootDirectory || ".");
|
|
2353
|
+
if (warnings.length > 0) allWarnings.push({
|
|
2354
|
+
service: svc.serviceName,
|
|
2355
|
+
warnings
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
if (allWarnings.length > 0) for (const { service, warnings } of allWarnings) output({
|
|
2359
|
+
type: "service_warnings",
|
|
2360
|
+
service,
|
|
2361
|
+
warnings: warnings.map((w) => ({
|
|
2362
|
+
message: w.message,
|
|
2363
|
+
hint: w.hint
|
|
2364
|
+
}))
|
|
2365
|
+
}, () => {
|
|
2366
|
+
console.log(chalk.yellow(`\n ${chalk.bold(service)}:`));
|
|
2367
|
+
printDeployWarnings(warnings);
|
|
2368
|
+
});
|
|
2369
|
+
for (const svc of services) {
|
|
2370
|
+
const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
|
|
2371
|
+
if (serviceConf) await syncServiceConfig(client, svc.serviceId, serviceConf);
|
|
2372
|
+
const detection = detectLocalRepo(serviceConf?.rootDirectory || ".");
|
|
2373
|
+
results.push({
|
|
1770
2374
|
...svc,
|
|
1771
2375
|
extraFiles: prepareExtraFiles(detection, serviceConf)
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
return results;
|
|
1774
2379
|
}
|
|
1775
|
-
async function triggerDeploy(serviceId, serviceName) {
|
|
2380
|
+
async function triggerDeploy(serviceId, serviceName, preDetection) {
|
|
1776
2381
|
const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
|
|
1777
2382
|
try {
|
|
1778
2383
|
const sizeInBytes = await calculateDirectorySize(process.cwd());
|
|
1779
2384
|
const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
|
|
1780
2385
|
if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
|
|
1781
2386
|
const client = await getClient();
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
}
|
|
2387
|
+
const serviceConf = resolveServiceConf(loadConfig(), serviceId);
|
|
2388
|
+
if (serviceConf) await syncServiceConfig(client, serviceId, serviceConf);
|
|
2389
|
+
const extraFiles = prepareExtraFiles(preDetection ?? detectLocalRepo(), serviceConf);
|
|
2390
|
+
const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
|
|
2391
|
+
if (warnings.length > 0) {
|
|
2392
|
+
spinUpload.stop();
|
|
2393
|
+
printDeployWarnings(warnings);
|
|
2394
|
+
spinUpload.start();
|
|
1798
2395
|
}
|
|
1799
|
-
const extraFiles = prepareExtraFiles(detectLocalRepo(), serviceConf);
|
|
1800
2396
|
spinUpload.text = "Iniciando deploy...";
|
|
1801
2397
|
const deployment = await withRetry(() => client.deployments.create({ serviceId }));
|
|
1802
2398
|
spinUpload.text = "Fazendo upload do código...";
|
|
1803
|
-
await withRetry(() => uploadSource(
|
|
2399
|
+
await withRetry(() => uploadSource(deployment.id, process.cwd(), extraFiles));
|
|
1804
2400
|
spinUpload.stop();
|
|
1805
|
-
success(
|
|
1806
|
-
|
|
2401
|
+
success(serviceName ? `Upload de ${chalk.bold(serviceName)} concluído` : "Upload concluído");
|
|
2402
|
+
setupSigintHandler();
|
|
2403
|
+
trackDeployment(deployment.id);
|
|
2404
|
+
try {
|
|
2405
|
+
await streamDeploymentLogs(deployment.id, serviceId, serviceName);
|
|
2406
|
+
} finally {
|
|
2407
|
+
untrackDeployment(deployment.id);
|
|
2408
|
+
}
|
|
1807
2409
|
} catch (error) {
|
|
1808
2410
|
spinUpload.stop();
|
|
1809
2411
|
handleError(error);
|
|
@@ -1833,19 +2435,26 @@ function readLocalFile(path) {
|
|
|
1833
2435
|
}
|
|
1834
2436
|
}
|
|
1835
2437
|
function printSummary(settings) {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2438
|
+
const { type: serviceType, ...rest } = settings;
|
|
2439
|
+
output({
|
|
2440
|
+
type: "service_summary",
|
|
2441
|
+
serviceType,
|
|
2442
|
+
...rest
|
|
2443
|
+
}, () => {
|
|
2444
|
+
console.log();
|
|
2445
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
2446
|
+
console.log(` ${chalk.bold("Nome:")} ${settings.name}`);
|
|
2447
|
+
console.log(` ${chalk.bold("Tipo:")} ${settings.type === "WEB" ? "Serviço Web" : "Site Estático"}`);
|
|
2448
|
+
console.log(` ${chalk.bold("Branch:")} ${settings.branch}`);
|
|
2449
|
+
if (settings.framework) console.log(` ${chalk.bold("Framework:")} ${settings.framework}`);
|
|
2450
|
+
if (settings.packageManager) console.log(` ${chalk.bold("Package Mgr:")} ${settings.packageManager}`);
|
|
2451
|
+
if (settings.buildCommand) console.log(` ${chalk.bold("Build:")} ${settings.buildCommand}`);
|
|
2452
|
+
if (settings.startCommand) console.log(` ${chalk.bold("Start:")} ${settings.startCommand}`);
|
|
2453
|
+
if (settings.outputDir) console.log(` ${chalk.bold("Output:")} ${settings.outputDir}`);
|
|
2454
|
+
if (settings.port) console.log(` ${chalk.bold("Porta:")} ${settings.port}`);
|
|
2455
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
2456
|
+
console.log();
|
|
2457
|
+
});
|
|
1849
2458
|
}
|
|
1850
2459
|
function detectLocalRepo(basePath = ".") {
|
|
1851
2460
|
const files = {};
|
|
@@ -1914,20 +2523,21 @@ function detectLocalRepo(basePath = ".") {
|
|
|
1914
2523
|
}
|
|
1915
2524
|
return analyzeRepo(files);
|
|
1916
2525
|
}
|
|
1917
|
-
async function promptEnvVars(
|
|
2526
|
+
async function promptEnvVars(serviceId, detectedVars) {
|
|
1918
2527
|
if (detectedVars.length === 0) return;
|
|
1919
2528
|
console.log(chalk.cyan(`\n📝 ${detectedVars.length} variável(is) de ambiente detectada(s):\n`));
|
|
1920
|
-
for (const v of detectedVars) console.log(` • ${v
|
|
2529
|
+
for (const v of detectedVars) console.log(` • ${v}`);
|
|
1921
2530
|
console.log();
|
|
1922
2531
|
if (!await promptConfirm("Deseja preencher as variáveis agora?", false)) return;
|
|
1923
2532
|
const vars = {};
|
|
1924
|
-
for (const
|
|
1925
|
-
const
|
|
1926
|
-
if (
|
|
2533
|
+
for (const key of detectedVars) {
|
|
2534
|
+
const val = await prompt(` ${chalk.bold(key)}:`);
|
|
2535
|
+
if (val) vars[key] = val;
|
|
1927
2536
|
}
|
|
1928
2537
|
const filled = Object.keys(vars).length;
|
|
1929
2538
|
if (filled > 0) {
|
|
1930
2539
|
const spinVars = spinner("Definindo variáveis de ambiente...");
|
|
2540
|
+
const client = await getClient();
|
|
1931
2541
|
await withRetry(() => client.envVars.setBulk({
|
|
1932
2542
|
serviceId,
|
|
1933
2543
|
vars
|
|
@@ -1936,7 +2546,8 @@ async function promptEnvVars(client, serviceId, detectedVars) {
|
|
|
1936
2546
|
success(`${filled} variável(is) definida(s).`);
|
|
1937
2547
|
}
|
|
1938
2548
|
}
|
|
1939
|
-
async function createServiceFlow(
|
|
2549
|
+
async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
2550
|
+
const client = await getClient();
|
|
1940
2551
|
const branch = getGitBranch();
|
|
1941
2552
|
const spinDetect = spinner("Detectando framework...");
|
|
1942
2553
|
const detection = detectLocalRepo();
|
|
@@ -1944,36 +2555,61 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
1944
2555
|
const pm = detection.packageManager;
|
|
1945
2556
|
if (detection.isMonorepo && detection.monorepoApps.length > 0) {
|
|
1946
2557
|
info(`Monorepo detectado (${pm})`);
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2558
|
+
const allApps = detection.monorepoApps.map((a) => ({
|
|
2559
|
+
name: a.name,
|
|
2560
|
+
root: a.path,
|
|
2561
|
+
framework: a.framework?.name ?? null,
|
|
2562
|
+
buildCommand: a.framework?.buildCommand ?? null,
|
|
2563
|
+
startCommand: a.framework?.startCommand ?? null,
|
|
2564
|
+
port: a.framework?.port ?? 3e3
|
|
2565
|
+
}));
|
|
2566
|
+
let selectedApps;
|
|
2567
|
+
if (opts.yes) if (opts.app) {
|
|
2568
|
+
selectedApps = allApps.filter((a) => a.root === opts.app);
|
|
2569
|
+
if (selectedApps.length === 0) {
|
|
2570
|
+
output({
|
|
2571
|
+
type: "error",
|
|
2572
|
+
message: `App '${opts.app}' não encontrado.`,
|
|
2573
|
+
available: allApps.map((a) => a.root)
|
|
2574
|
+
}, () => {
|
|
2575
|
+
const available = allApps.map((a) => ` • ${a.root}`).join("\n");
|
|
2576
|
+
console.error(chalk.red(`\n✗ App '${opts.app}' não encontrado.\n\nApps disponíveis:\n${available}`));
|
|
2577
|
+
});
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
} else selectedApps = allApps;
|
|
2581
|
+
else {
|
|
2582
|
+
const selectedPaths = await promptMultiSelect("Quais apps deseja fazer o deploy?", allApps.map((app) => ({
|
|
2583
|
+
label: `${app.name}${app.framework ? ` (${app.framework})` : ""} — ${app.root}`,
|
|
2584
|
+
value: app.root
|
|
2585
|
+
})));
|
|
2586
|
+
selectedApps = allApps.filter((a) => selectedPaths.includes(a.root));
|
|
2587
|
+
}
|
|
1952
2588
|
for (const app of selectedApps) {
|
|
1953
|
-
const fw$1 = app.framework;
|
|
1954
2589
|
console.log(chalk.cyan(`\n── ${app.name} ──`));
|
|
1955
2590
|
printSummary({
|
|
1956
2591
|
name: app.name,
|
|
1957
|
-
type:
|
|
1958
|
-
rootDir: app.
|
|
2592
|
+
type: "WEB",
|
|
2593
|
+
rootDir: app.root,
|
|
1959
2594
|
branch,
|
|
1960
|
-
framework:
|
|
2595
|
+
framework: app.framework,
|
|
1961
2596
|
packageManager: pm,
|
|
1962
|
-
buildCommand:
|
|
1963
|
-
startCommand:
|
|
1964
|
-
outputDir:
|
|
1965
|
-
port:
|
|
2597
|
+
buildCommand: app.buildCommand,
|
|
2598
|
+
startCommand: app.startCommand,
|
|
2599
|
+
outputDir: null,
|
|
2600
|
+
port: app.port ?? 3e3
|
|
1966
2601
|
});
|
|
1967
2602
|
}
|
|
1968
|
-
if (!
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2603
|
+
if (!opts.yes) {
|
|
2604
|
+
if (!await promptConfirm("Confirmar e fazer deploy?")) for (const app of selectedApps) {
|
|
2605
|
+
console.log(chalk.cyan(`\n── Editar: ${app.name} ──`));
|
|
2606
|
+
const newBuild = await prompt(` Build command: ${chalk.dim(`(${app.buildCommand ?? "—"})`)}`);
|
|
2607
|
+
if (newBuild) app.buildCommand = newBuild;
|
|
2608
|
+
const newStart = await prompt(` Start command: ${chalk.dim(`(${app.startCommand ?? "—"})`)}`);
|
|
2609
|
+
if (newStart) app.startCommand = newStart;
|
|
2610
|
+
const newPort = await prompt(` Port: ${chalk.dim(`(${app.port ?? 3e3})`)}`);
|
|
2611
|
+
if (newPort) app.port = parseInt(newPort, 10) || app.port;
|
|
2612
|
+
}
|
|
1977
2613
|
}
|
|
1978
2614
|
const config = {
|
|
1979
2615
|
version: "1.0",
|
|
@@ -1986,18 +2622,17 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
1986
2622
|
};
|
|
1987
2623
|
const createdServices = [];
|
|
1988
2624
|
for (const app of selectedApps) {
|
|
1989
|
-
const fw$1 = app.framework;
|
|
1990
2625
|
console.log(chalk.cyan(`\n── Criando serviço: ${app.name} ──\n`));
|
|
1991
2626
|
const spinService$1 = spinner(`Criando serviço ${chalk.bold(app.name)}...`);
|
|
1992
2627
|
const service$1 = await withRetry(() => client.services.create({
|
|
1993
2628
|
projectId,
|
|
1994
2629
|
name: app.name,
|
|
1995
|
-
type:
|
|
2630
|
+
type: "WEB",
|
|
1996
2631
|
branch,
|
|
1997
|
-
rootDirectory: app.
|
|
1998
|
-
buildCommand:
|
|
1999
|
-
startCommand:
|
|
2000
|
-
port:
|
|
2632
|
+
rootDirectory: app.root,
|
|
2633
|
+
buildCommand: app.buildCommand ?? void 0,
|
|
2634
|
+
startCommand: app.startCommand ?? void 0,
|
|
2635
|
+
port: app.port ?? 3e3
|
|
2001
2636
|
}));
|
|
2002
2637
|
spinService$1.stop();
|
|
2003
2638
|
success(`Serviço criado: ${chalk.bold(service$1.name)}`);
|
|
@@ -2005,40 +2640,36 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2005
2640
|
service: service$1,
|
|
2006
2641
|
app
|
|
2007
2642
|
});
|
|
2008
|
-
config.services[app.
|
|
2643
|
+
config.services[app.root] = {
|
|
2009
2644
|
id: service$1.id,
|
|
2010
2645
|
name: service$1.name,
|
|
2011
|
-
type:
|
|
2012
|
-
root: app.
|
|
2646
|
+
type: "web",
|
|
2647
|
+
root: app.root,
|
|
2013
2648
|
branch,
|
|
2014
|
-
build:
|
|
2015
|
-
command: fw$1?.buildCommand ?? void 0,
|
|
2016
|
-
outputDir: fw$1?.outputDir ?? void 0
|
|
2017
|
-
} : void 0,
|
|
2649
|
+
build: app.buildCommand ? { command: app.buildCommand ?? void 0 } : void 0,
|
|
2018
2650
|
runtime: {
|
|
2019
|
-
command:
|
|
2020
|
-
port:
|
|
2651
|
+
command: app.startCommand ?? void 0,
|
|
2652
|
+
port: app.port ?? 3e3
|
|
2021
2653
|
}
|
|
2022
2654
|
};
|
|
2023
2655
|
}
|
|
2024
2656
|
saveConfig(config);
|
|
2025
2657
|
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2026
|
-
for (const { service: service$1, app } of createdServices) {
|
|
2658
|
+
if (!opts.yes) for (const { service: service$1, app } of createdServices) {
|
|
2027
2659
|
console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
|
|
2028
|
-
const serviceDetection = detectLocalRepo(app.
|
|
2029
|
-
await promptEnvVars(
|
|
2660
|
+
const serviceDetection = detectLocalRepo(app.root);
|
|
2661
|
+
await promptEnvVars(service$1.id, serviceDetection.envVars.map((v) => v.key));
|
|
2030
2662
|
}
|
|
2031
|
-
await deployServicesInParallel(
|
|
2663
|
+
await deployServicesInParallel(createdServices.map(({ service: service$1, app }) => ({
|
|
2032
2664
|
serviceId: service$1.id,
|
|
2033
2665
|
serviceName: app.name,
|
|
2034
|
-
path: resolve(process.cwd(), app.
|
|
2035
|
-
extraFiles: prepareExtraFiles(detectLocalRepo(app.
|
|
2036
|
-
type:
|
|
2037
|
-
buildCommand: app.
|
|
2038
|
-
startCommand: app.
|
|
2039
|
-
port: app.
|
|
2040
|
-
rootDirectory: app.
|
|
2041
|
-
outputDir: app.framework?.outputDir ?? void 0
|
|
2666
|
+
path: resolve(process.cwd(), app.root),
|
|
2667
|
+
extraFiles: prepareExtraFiles(detectLocalRepo(app.root), {
|
|
2668
|
+
type: "WEB",
|
|
2669
|
+
buildCommand: app.buildCommand ?? void 0,
|
|
2670
|
+
startCommand: app.startCommand ?? void 0,
|
|
2671
|
+
port: app.port ?? void 0,
|
|
2672
|
+
rootDirectory: app.root
|
|
2042
2673
|
})
|
|
2043
2674
|
})));
|
|
2044
2675
|
return createdServices[createdServices.length - 1]?.service.id || "";
|
|
@@ -2049,31 +2680,33 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2049
2680
|
type: fw?.type ?? "WEB",
|
|
2050
2681
|
rootDir: ".",
|
|
2051
2682
|
branch,
|
|
2052
|
-
framework: fw?.
|
|
2683
|
+
framework: fw?.name ?? null,
|
|
2053
2684
|
packageManager: pm,
|
|
2054
2685
|
buildCommand: fw?.buildCommand ?? null,
|
|
2055
2686
|
startCommand: fw?.startCommand ?? null,
|
|
2056
2687
|
outputDir: fw?.outputDir ?? null,
|
|
2057
2688
|
port: fw?.port ?? 3e3
|
|
2058
2689
|
};
|
|
2059
|
-
if (fw) info(`Framework detectado: ${chalk.bold(fw.
|
|
2690
|
+
if (fw) info(`Framework detectado: ${chalk.bold(fw.name)}`);
|
|
2060
2691
|
printSummary(settings);
|
|
2061
|
-
if (!
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2692
|
+
if (!opts.yes) {
|
|
2693
|
+
if (!await promptConfirm("Confirmar e fazer deploy?")) {
|
|
2694
|
+
const newName = await prompt(`Nome do serviço: ${chalk.dim(`(${settings.name})`)}`);
|
|
2695
|
+
if (newName) settings.name = newName;
|
|
2696
|
+
settings.type = await promptSelect("Tipo de serviço:", [{
|
|
2697
|
+
label: "Serviço Web",
|
|
2698
|
+
value: "WEB"
|
|
2699
|
+
}, {
|
|
2700
|
+
label: "Site Estático",
|
|
2701
|
+
value: "STATIC"
|
|
2702
|
+
}]);
|
|
2703
|
+
const newBuild = await prompt(`Build command: ${chalk.dim(`(${settings.buildCommand ?? "—"})`)}`);
|
|
2704
|
+
if (newBuild) settings.buildCommand = newBuild;
|
|
2705
|
+
const newStart = await prompt(`Start command: ${chalk.dim(`(${settings.startCommand ?? "—"})`)}`);
|
|
2706
|
+
if (newStart) settings.startCommand = newStart;
|
|
2707
|
+
const newPort = await prompt(`Port: ${chalk.dim(`(${settings.port})`)}`);
|
|
2708
|
+
if (newPort) settings.port = parseInt(newPort, 10) || settings.port;
|
|
2709
|
+
}
|
|
2077
2710
|
}
|
|
2078
2711
|
const spinService = spinner("Criando serviço...");
|
|
2079
2712
|
const service = await withRetry(() => client.services.create({
|
|
@@ -2088,7 +2721,7 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2088
2721
|
}));
|
|
2089
2722
|
spinService.stop();
|
|
2090
2723
|
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2091
|
-
await promptEnvVars(
|
|
2724
|
+
if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key));
|
|
2092
2725
|
saveConfig({
|
|
2093
2726
|
version: "1.0",
|
|
2094
2727
|
project: {
|
|
@@ -2115,24 +2748,129 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2115
2748
|
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2116
2749
|
return service.id;
|
|
2117
2750
|
}
|
|
2118
|
-
|
|
2751
|
+
/**
|
|
2752
|
+
* Create a new environment by mirroring the base config's services
|
|
2753
|
+
* into a new project on the platform. Saves the environment mapping to veloz.json.
|
|
2754
|
+
*/
|
|
2755
|
+
async function createEnvironmentFlow(rawConfig, envName, opts) {
|
|
2756
|
+
const client = await getClient();
|
|
2757
|
+
const projectName = `${rawConfig.project.name}-${envName}`;
|
|
2758
|
+
info(`Criando ambiente "${envName}" — projeto: ${chalk.bold(projectName)}`);
|
|
2759
|
+
if (!opts.yes) {
|
|
2760
|
+
if (!await promptConfirm(`Criar ambiente "${envName}" com ${Object.keys(rawConfig.services).length} serviço(s)?`)) {
|
|
2761
|
+
info("Criação de ambiente cancelada.");
|
|
2762
|
+
process.exit(0);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
const remote = getGitRemote();
|
|
2766
|
+
const spinProject = spinner(`Criando projeto ${chalk.bold(projectName)}...`);
|
|
2767
|
+
const newProject = await withRetry(() => client.projects.create({
|
|
2768
|
+
name: projectName,
|
|
2769
|
+
githubRepoOwner: remote?.owner,
|
|
2770
|
+
githubRepoName: remote?.repo
|
|
2771
|
+
}));
|
|
2772
|
+
spinProject.stop();
|
|
2773
|
+
success(`Projeto criado: ${chalk.bold(newProject.name)}`);
|
|
2774
|
+
const envServices = {};
|
|
2775
|
+
for (const [key, serviceConfig] of Object.entries(rawConfig.services)) {
|
|
2776
|
+
const serviceName = `${serviceConfig.name}-${envName}`;
|
|
2777
|
+
const spinService = spinner(`Criando serviço ${chalk.bold(serviceName)}...`);
|
|
2778
|
+
const service = await withRetry(() => client.services.create({
|
|
2779
|
+
projectId: newProject.id,
|
|
2780
|
+
name: serviceName,
|
|
2781
|
+
type: serviceConfig.type?.toUpperCase() ?? "WEB",
|
|
2782
|
+
branch: serviceConfig.branch ?? "main",
|
|
2783
|
+
rootDirectory: serviceConfig.root,
|
|
2784
|
+
buildCommand: serviceConfig.build?.command ?? void 0,
|
|
2785
|
+
startCommand: serviceConfig.runtime?.command ?? void 0,
|
|
2786
|
+
port: serviceConfig.runtime?.port ?? 3e3
|
|
2787
|
+
}));
|
|
2788
|
+
spinService.stop();
|
|
2789
|
+
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2790
|
+
envServices[key] = {
|
|
2791
|
+
id: service.id,
|
|
2792
|
+
name: serviceName
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
const envOverride = {
|
|
2796
|
+
project: {
|
|
2797
|
+
id: newProject.id,
|
|
2798
|
+
name: newProject.name
|
|
2799
|
+
},
|
|
2800
|
+
services: envServices
|
|
2801
|
+
};
|
|
2802
|
+
saveConfig({
|
|
2803
|
+
...rawConfig,
|
|
2804
|
+
environments: {
|
|
2805
|
+
...rawConfig.environments,
|
|
2806
|
+
[envName]: envOverride
|
|
2807
|
+
},
|
|
2808
|
+
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2809
|
+
});
|
|
2810
|
+
success(`Ambiente "${envName}" salvo em ${getConfigFileName()}`);
|
|
2811
|
+
setActiveEnv(envName);
|
|
2812
|
+
return loadConfig();
|
|
2813
|
+
}
|
|
2814
|
+
function isMultipleOrgsError(error) {
|
|
2815
|
+
if (!(error instanceof Error)) return false;
|
|
2816
|
+
const e = error;
|
|
2817
|
+
return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
|
|
2818
|
+
}
|
|
2819
|
+
async function resolveOrgIfNeeded(nonInteractive) {
|
|
2820
|
+
if ((await requireAuth({ nonInteractive })).organizationId) return;
|
|
2821
|
+
if (loadConfig()?.project?.id) return;
|
|
2822
|
+
const client = await getClient();
|
|
2823
|
+
try {
|
|
2824
|
+
await client.projects.list();
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
if (!isMultipleOrgsError(error)) throw error;
|
|
2827
|
+
const orgs = error.data.organizations;
|
|
2828
|
+
info(`Você tem acesso a ${orgs.length} organizações.`);
|
|
2829
|
+
saveConfig$1({ organizationId: await promptSelect("Selecione a organização para este deploy:", orgs.map((org) => ({
|
|
2830
|
+
label: `${org.name} ${chalk.dim(`(${org.slug})`)}`,
|
|
2831
|
+
value: org.id
|
|
2832
|
+
}))) });
|
|
2833
|
+
success("Organização salva.");
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
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("--app <path>", "App do monorepo para deploy (ex: apps/web)").option("-y, --yes", "Auto-confirmar tudo (modo não-interativo)").option("-v, --verbose", "Mostrar logs detalhados do servidor").action(async (options) => {
|
|
2119
2837
|
if (options.verbose) process.env.VELOZ_VERBOSE = "true";
|
|
2120
2838
|
try {
|
|
2121
|
-
|
|
2839
|
+
printBanner("Deploy");
|
|
2840
|
+
await autoUpdate();
|
|
2841
|
+
await requireAuth({ nonInteractive: options.yes });
|
|
2842
|
+
await resolveOrgIfNeeded(options.yes);
|
|
2843
|
+
const activeEnv = getActiveEnv();
|
|
2844
|
+
if (activeEnv) {
|
|
2845
|
+
const rawConfig = loadRawConfig();
|
|
2846
|
+
if (rawConfig && !rawConfig.environments?.[activeEnv]) {
|
|
2847
|
+
info(`Ambiente "${activeEnv}" não encontrado. Configurando...`);
|
|
2848
|
+
await createEnvironmentFlow(rawConfig, activeEnv, { yes: options.yes });
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2122
2851
|
const configuredServices = await findServicesFromConfig();
|
|
2123
2852
|
if (configuredServices.length > 0) {
|
|
2124
2853
|
if (options.service) {
|
|
2125
2854
|
const found = configuredServices.find((s) => s.key === options.service || s.serviceName.toLowerCase() === options.service.toLowerCase() || s.serviceId === options.service);
|
|
2126
2855
|
if (!found) {
|
|
2127
|
-
|
|
2128
|
-
|
|
2856
|
+
output({
|
|
2857
|
+
type: "error",
|
|
2858
|
+
message: `Serviço '${options.service}' não encontrado.`,
|
|
2859
|
+
available: configuredServices.map((s) => ({
|
|
2860
|
+
key: s.key,
|
|
2861
|
+
name: s.serviceName
|
|
2862
|
+
}))
|
|
2863
|
+
}, () => {
|
|
2864
|
+
const available = configuredServices.map((s) => ` • ${s.key} (${s.serviceName})`).join("\n");
|
|
2865
|
+
console.error(chalk.red(`\n✗ Serviço '${options.service}' não encontrado.\n\nServiços disponíveis:\n${available}`));
|
|
2866
|
+
});
|
|
2129
2867
|
process.exit(1);
|
|
2130
2868
|
}
|
|
2131
2869
|
await triggerDeploy(found.serviceId, found.serviceName);
|
|
2132
2870
|
return;
|
|
2133
2871
|
}
|
|
2134
|
-
if (options.all || configuredServices.length === 1) {
|
|
2135
|
-
if (configuredServices.length > 1) {
|
|
2872
|
+
if (options.all || options.yes || configuredServices.length === 1) {
|
|
2873
|
+
if (configuredServices.length > 1 && !options.yes) {
|
|
2136
2874
|
console.log(chalk.cyan(`\n🚀 Fazendo deploy de ${configuredServices.length} serviço(s):\n`));
|
|
2137
2875
|
for (const service of configuredServices) {
|
|
2138
2876
|
const relPath = relative(process.cwd(), service.path) || ".";
|
|
@@ -2145,10 +2883,7 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2145
2883
|
}
|
|
2146
2884
|
}
|
|
2147
2885
|
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
|
-
}
|
|
2886
|
+
else await deployServicesInParallel(await computeExtraFilesForServices(configuredServices));
|
|
2152
2887
|
return;
|
|
2153
2888
|
} else {
|
|
2154
2889
|
console.log(chalk.bold("\n📦 Serviços disponíveis:\n"));
|
|
@@ -2165,24 +2900,21 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2165
2900
|
return;
|
|
2166
2901
|
}
|
|
2167
2902
|
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
|
-
}
|
|
2903
|
+
else await deployServicesInParallel(await computeExtraFilesForServices(selectedServices));
|
|
2172
2904
|
return;
|
|
2173
2905
|
}
|
|
2174
2906
|
}
|
|
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
|
-
}
|
|
2907
|
+
if (!isGitRepo()) handleError(/* @__PURE__ */ new Error("Este diretório não é um repositório git. Inicialize com `git init` e adicione um remote."));
|
|
2179
2908
|
info("Detectando repositório git...");
|
|
2180
2909
|
const remote = getGitRemote();
|
|
2181
|
-
if (!remote)
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2910
|
+
if (!remote) handleError(/* @__PURE__ */ new Error("Nenhum remote 'origin' encontrado. Adicione um remote git."));
|
|
2911
|
+
output({
|
|
2912
|
+
type: "git_repo",
|
|
2913
|
+
owner: remote.owner,
|
|
2914
|
+
repo: remote.repo
|
|
2915
|
+
}, () => {
|
|
2916
|
+
console.log(chalk.white(` ${chalk.bold("Repositório:")} ${remote.owner}/${remote.repo}`));
|
|
2917
|
+
});
|
|
2186
2918
|
const client = await getClient();
|
|
2187
2919
|
const spin = spinner("Buscando projeto...");
|
|
2188
2920
|
const project = await withRetry(() => client.projects.findByRepo({
|
|
@@ -2198,6 +2930,10 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2198
2930
|
const svc = project.services[0];
|
|
2199
2931
|
serviceId = svc.id;
|
|
2200
2932
|
serviceName = svc.name;
|
|
2933
|
+
} else if (options.yes) {
|
|
2934
|
+
const svc = project.services[0];
|
|
2935
|
+
serviceId = svc.id;
|
|
2936
|
+
serviceName = svc.name;
|
|
2201
2937
|
} else {
|
|
2202
2938
|
serviceId = await promptSelect("Selecione o serviço:", project.services.map((s) => ({
|
|
2203
2939
|
label: `${s.name} (${s.type} — branch: ${s.branch})`,
|
|
@@ -2206,6 +2942,8 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2206
2942
|
serviceName = project.services.find((s) => s.id === serviceId)?.name ?? "";
|
|
2207
2943
|
}
|
|
2208
2944
|
const selectedService = project.services.find((s) => s.id === serviceId);
|
|
2945
|
+
const detection = detectLocalRepo();
|
|
2946
|
+
const detectedType = selectedService?.type?.toLowerCase() ?? "web";
|
|
2209
2947
|
saveConfig({
|
|
2210
2948
|
version: "1.0",
|
|
2211
2949
|
project: {
|
|
@@ -2215,24 +2953,27 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2215
2953
|
services: { main: {
|
|
2216
2954
|
id: serviceId,
|
|
2217
2955
|
name: serviceName,
|
|
2218
|
-
type:
|
|
2956
|
+
type: detectedType,
|
|
2219
2957
|
root: ".",
|
|
2220
2958
|
branch: selectedService?.branch
|
|
2221
2959
|
} },
|
|
2222
2960
|
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2223
2961
|
});
|
|
2224
2962
|
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2225
|
-
await triggerDeploy(serviceId);
|
|
2963
|
+
await triggerDeploy(serviceId, void 0, detection);
|
|
2226
2964
|
return;
|
|
2227
2965
|
}
|
|
2228
2966
|
if (project && project.services.length === 0) {
|
|
2229
2967
|
info(`Projeto encontrado: ${chalk.bold(project.name)}`);
|
|
2230
2968
|
info("Nenhum serviço configurado. Vamos criar um.");
|
|
2231
|
-
await triggerDeploy(await createServiceFlow(
|
|
2969
|
+
await triggerDeploy(await createServiceFlow(project.id, project.name, remote.repo, {
|
|
2970
|
+
yes: options.yes,
|
|
2971
|
+
app: options.app
|
|
2972
|
+
}));
|
|
2232
2973
|
return;
|
|
2233
2974
|
}
|
|
2234
2975
|
info("Projeto não encontrado. Vamos criar um novo.");
|
|
2235
|
-
const projectName = await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
|
|
2976
|
+
const projectName = options.yes ? remote.repo : await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
|
|
2236
2977
|
const spinProject = spinner("Criando projeto...");
|
|
2237
2978
|
const newProject = await withRetry(() => client.projects.create({
|
|
2238
2979
|
name: projectName,
|
|
@@ -2241,7 +2982,10 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2241
2982
|
}));
|
|
2242
2983
|
spinProject.stop();
|
|
2243
2984
|
success(`Projeto criado: ${chalk.bold(newProject.name)}`);
|
|
2244
|
-
await triggerDeploy(await createServiceFlow(
|
|
2985
|
+
await triggerDeploy(await createServiceFlow(newProject.id, newProject.name, remote.repo, {
|
|
2986
|
+
yes: options.yes,
|
|
2987
|
+
app: options.app
|
|
2988
|
+
}));
|
|
2245
2989
|
} catch (error) {
|
|
2246
2990
|
handleError(error);
|
|
2247
2991
|
}
|
|
@@ -2283,10 +3027,14 @@ const TAG_COLORS = [
|
|
|
2283
3027
|
chalk.red
|
|
2284
3028
|
];
|
|
2285
3029
|
function getServiceTag(name, maxLen, colorIndex) {
|
|
3030
|
+
const mode = getOutputMode();
|
|
3031
|
+
if (mode === "json" || mode === "github-actions" || mode === "plain") return `[${name.padEnd(maxLen)}]`;
|
|
2286
3032
|
const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
|
|
2287
3033
|
return color(`[${name.padEnd(maxLen)}]`);
|
|
2288
3034
|
}
|
|
2289
3035
|
function getServiceHeader(name, colorIndex) {
|
|
3036
|
+
const mode = getOutputMode();
|
|
3037
|
+
if (mode === "json" || mode === "github-actions" || mode === "plain") return `── ${name} ──`;
|
|
2290
3038
|
const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
|
|
2291
3039
|
return color(`── ${name} ──`);
|
|
2292
3040
|
}
|
|
@@ -2385,7 +3133,8 @@ function resolveAllServices(serviceFlag) {
|
|
|
2385
3133
|
function formatTime(timestamp) {
|
|
2386
3134
|
return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
|
|
2387
3135
|
}
|
|
2388
|
-
async function streamFollow(
|
|
3136
|
+
async function streamFollow(services, maxNameLen, tailLines) {
|
|
3137
|
+
const client = await getClient();
|
|
2389
3138
|
const showTags = services.length > 1;
|
|
2390
3139
|
const streams = services.map(async ({ service, index }) => {
|
|
2391
3140
|
const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
|
|
@@ -2394,14 +3143,22 @@ async function streamFollow(client, services, maxNameLen, tailLines) {
|
|
|
2394
3143
|
serviceId: service.id,
|
|
2395
3144
|
tailLines: Math.ceil(tailLines / services.length)
|
|
2396
3145
|
});
|
|
2397
|
-
for await (const entry of stream)
|
|
3146
|
+
for await (const entry of stream) output({
|
|
3147
|
+
type: "log_entry",
|
|
3148
|
+
service: service.name,
|
|
3149
|
+
timestamp: entry.timestamp,
|
|
3150
|
+
message: entry.message
|
|
3151
|
+
}, () => {
|
|
3152
|
+
console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
|
|
3153
|
+
});
|
|
2398
3154
|
} catch {
|
|
2399
3155
|
console.log(`${tag}${chalk.red("Erro ao conectar ao stream de logs")}`);
|
|
2400
3156
|
}
|
|
2401
3157
|
});
|
|
2402
3158
|
await Promise.allSettled(streams);
|
|
2403
3159
|
}
|
|
2404
|
-
async function fetchRecent(
|
|
3160
|
+
async function fetchRecent(services, maxNameLen, tailLines) {
|
|
3161
|
+
const client = await getClient();
|
|
2405
3162
|
const showTags = services.length > 1;
|
|
2406
3163
|
const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
|
|
2407
3164
|
return (await client.logs.getRecent({
|
|
@@ -2417,28 +3174,36 @@ async function fetchRecent(client, services, maxNameLen, tailLines) {
|
|
|
2417
3174
|
info("Nenhum log encontrado.");
|
|
2418
3175
|
return;
|
|
2419
3176
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
3177
|
+
output({
|
|
3178
|
+
type: "logs",
|
|
3179
|
+
entries: allEntries.map((e) => ({
|
|
3180
|
+
service: e.serviceName,
|
|
3181
|
+
timestamp: e.timestamp,
|
|
3182
|
+
message: e.message
|
|
3183
|
+
}))
|
|
3184
|
+
}, () => {
|
|
3185
|
+
console.log();
|
|
3186
|
+
for (const entry of allEntries) {
|
|
3187
|
+
const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
|
|
3188
|
+
console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
|
|
3189
|
+
}
|
|
3190
|
+
console.log();
|
|
3191
|
+
info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
|
|
3192
|
+
});
|
|
2427
3193
|
}
|
|
2428
3194
|
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
3195
|
const spin = spinner("Carregando logs...");
|
|
2430
3196
|
try {
|
|
2431
3197
|
const { services, maxNameLen } = resolveAllServices(opts.service);
|
|
2432
|
-
const client = await getClient();
|
|
2433
3198
|
const tailLines = parseInt(opts.tail, 10) || 50;
|
|
2434
3199
|
if (opts.follow) {
|
|
2435
3200
|
spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
|
|
2436
3201
|
spin.stop();
|
|
2437
3202
|
info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
|
|
2438
|
-
await streamFollow(
|
|
3203
|
+
await streamFollow(services, maxNameLen, tailLines);
|
|
2439
3204
|
} else {
|
|
2440
3205
|
spin.stop();
|
|
2441
|
-
await fetchRecent(
|
|
3206
|
+
await fetchRecent(services, maxNameLen, tailLines);
|
|
2442
3207
|
}
|
|
2443
3208
|
} catch (error) {
|
|
2444
3209
|
spin.stop();
|
|
@@ -2472,7 +3237,11 @@ envCommand.command("list").alias("listar").description("Listar variáveis de amb
|
|
|
2472
3237
|
chalk.bold(v.key),
|
|
2473
3238
|
chalk.dim(v.maskedValue),
|
|
2474
3239
|
new Date(v.updatedAt).toLocaleDateString("pt-BR")
|
|
2475
|
-
]))
|
|
3240
|
+
]), envVars.map((v) => ({
|
|
3241
|
+
key: v.key,
|
|
3242
|
+
maskedValue: v.maskedValue,
|
|
3243
|
+
updatedAt: v.updatedAt
|
|
3244
|
+
})));
|
|
2476
3245
|
}
|
|
2477
3246
|
spin.stop();
|
|
2478
3247
|
if (totalVars === 0 && !showHeaders) info("Nenhuma variável de ambiente configurada.");
|
|
@@ -2490,15 +3259,13 @@ envCommand.command("set <pares...>").description("Definir variável de ambiente
|
|
|
2490
3259
|
const eqIndex = par.indexOf("=");
|
|
2491
3260
|
if (eqIndex === -1) {
|
|
2492
3261
|
spin.stop();
|
|
2493
|
-
|
|
2494
|
-
process.exit(1);
|
|
3262
|
+
handleError(/* @__PURE__ */ new Error(`Formato inválido: "${par}". Use CHAVE=VALOR.`));
|
|
2495
3263
|
}
|
|
2496
3264
|
const key = par.slice(0, eqIndex);
|
|
2497
3265
|
const value = par.slice(eqIndex + 1);
|
|
2498
3266
|
if (!key) {
|
|
2499
3267
|
spin.stop();
|
|
2500
|
-
|
|
2501
|
-
process.exit(1);
|
|
3268
|
+
handleError(/* @__PURE__ */ new Error("Chave não pode estar vazia."));
|
|
2502
3269
|
}
|
|
2503
3270
|
await client.envVars.set({
|
|
2504
3271
|
serviceId,
|
|
@@ -2540,38 +3307,32 @@ envCommand.command("import [arquivo]").description("Importar variáveis de ambie
|
|
|
2540
3307
|
let envContent = "";
|
|
2541
3308
|
if (arquivo) {
|
|
2542
3309
|
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
|
-
}
|
|
3310
|
+
if (!existsSync(filePath)) handleError(/* @__PURE__ */ new Error(`Arquivo não encontrado: ${arquivo}`));
|
|
2547
3311
|
envContent = readFileSync(filePath, "utf-8");
|
|
2548
3312
|
info(`Importando de ${chalk.bold(arquivo)}...`);
|
|
2549
3313
|
} 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
3314
|
const rl = readline.createInterface({
|
|
2553
3315
|
input: process.stdin,
|
|
2554
|
-
output: process.stdout
|
|
3316
|
+
output: isInteractive() ? process.stdout : void 0
|
|
2555
3317
|
});
|
|
2556
|
-
|
|
3318
|
+
if (isInteractive()) {
|
|
3319
|
+
console.log(chalk.cyan("\n📋 Modo de colagem interativo"));
|
|
3320
|
+
console.log(chalk.dim("Cole seu conteúdo .env abaixo. Pressione Ctrl+D (ou Ctrl+Z no Windows) quando terminar:\n"));
|
|
3321
|
+
}
|
|
3322
|
+
const lines = [];
|
|
2557
3323
|
await new Promise((resolve$1) => {
|
|
2558
|
-
rl.on("line", (line) =>
|
|
2559
|
-
|
|
2560
|
-
});
|
|
2561
|
-
rl.on("close", () => {
|
|
2562
|
-
resolve$1();
|
|
2563
|
-
});
|
|
3324
|
+
rl.on("line", (line) => lines.push(line));
|
|
3325
|
+
rl.on("close", () => resolve$1());
|
|
2564
3326
|
});
|
|
2565
|
-
envContent = lines
|
|
3327
|
+
envContent = lines.join("\n");
|
|
2566
3328
|
}
|
|
2567
3329
|
const envVars = {};
|
|
2568
|
-
const
|
|
2569
|
-
for (const line of lines) {
|
|
3330
|
+
for (const line of envContent.split("\n")) {
|
|
2570
3331
|
const trimmed = line.trim();
|
|
2571
3332
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2572
3333
|
const eqIndex = trimmed.indexOf("=");
|
|
2573
3334
|
if (eqIndex === -1) continue;
|
|
2574
|
-
|
|
3335
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
2575
3336
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2576
3337
|
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2577
3338
|
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
|
|
@@ -2579,7 +3340,7 @@ envCommand.command("import [arquivo]").description("Importar variáveis de ambie
|
|
|
2579
3340
|
}
|
|
2580
3341
|
const varsCount = Object.keys(envVars).length;
|
|
2581
3342
|
if (varsCount === 0) {
|
|
2582
|
-
|
|
3343
|
+
warn("Nenhuma variável válida encontrada.");
|
|
2583
3344
|
return;
|
|
2584
3345
|
}
|
|
2585
3346
|
console.log(chalk.bold("\n📝 Variáveis a serem importadas:\n"));
|
|
@@ -2628,20 +3389,31 @@ envCommand.command("export [arquivo]").description("Exportar variáveis de ambie
|
|
|
2628
3389
|
return;
|
|
2629
3390
|
}
|
|
2630
3391
|
const envContent = envVars.map((v) => `${v.key}=${v.maskedValue}`).join("\n");
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
3392
|
+
output({
|
|
3393
|
+
type: "env_export",
|
|
3394
|
+
vars: envVars.map((v) => ({
|
|
3395
|
+
key: v.key,
|
|
3396
|
+
maskedValue: v.maskedValue
|
|
3397
|
+
}))
|
|
3398
|
+
}, () => {
|
|
3399
|
+
if (arquivo) {
|
|
3400
|
+
writeFileSync(resolve(process.cwd(), arquivo), envContent + "\n", "utf-8");
|
|
3401
|
+
success(`Variáveis exportadas para ${chalk.bold(arquivo)}`);
|
|
3402
|
+
console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
|
|
3403
|
+
} else {
|
|
3404
|
+
console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
|
|
3405
|
+
console.log(envContent);
|
|
3406
|
+
console.log();
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
2640
3409
|
} catch (error) {
|
|
2641
3410
|
spin.stop();
|
|
2642
3411
|
handleError(error);
|
|
2643
3412
|
}
|
|
2644
3413
|
});
|
|
3414
|
+
function warn(message) {
|
|
3415
|
+
console.log(chalk.yellow(`⚠ ${message}`));
|
|
3416
|
+
}
|
|
2645
3417
|
|
|
2646
3418
|
//#endregion
|
|
2647
3419
|
//#region src/commands/domains.ts
|
|
@@ -2674,12 +3446,18 @@ domainsCommand.command("list").alias("listar").description("Listar domínios dos
|
|
|
2674
3446
|
"TLS",
|
|
2675
3447
|
"Tipo"
|
|
2676
3448
|
], domains.map((d) => [
|
|
2677
|
-
d.id
|
|
3449
|
+
d.id,
|
|
2678
3450
|
chalk.bold(d.domain),
|
|
2679
3451
|
d.verified ? chalk.green("✓ Sim") : chalk.yellow("✗ Não"),
|
|
2680
3452
|
tlsStatusLabels[d.tlsStatus] ?? d.tlsStatus,
|
|
2681
3453
|
d.isAutoGenerated ? "Auto" : "Personalizado"
|
|
2682
|
-
]))
|
|
3454
|
+
]), domains.map((d) => ({
|
|
3455
|
+
id: d.id,
|
|
3456
|
+
domain: d.domain,
|
|
3457
|
+
verified: d.verified,
|
|
3458
|
+
tlsStatus: d.tlsStatus,
|
|
3459
|
+
isAutoGenerated: d.isAutoGenerated
|
|
3460
|
+
})));
|
|
2683
3461
|
}
|
|
2684
3462
|
spin.stop();
|
|
2685
3463
|
if (totalDomains === 0 && !showHeaders) info("Nenhum domínio configurado.");
|
|
@@ -2697,12 +3475,19 @@ domainsCommand.command("add <dominio>").alias("adicionar").description("Adiciona
|
|
|
2697
3475
|
domain: dominio
|
|
2698
3476
|
});
|
|
2699
3477
|
spin.stop();
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
3478
|
+
output({
|
|
3479
|
+
type: "domain_added",
|
|
3480
|
+
id: result.id,
|
|
3481
|
+
domain: dominio,
|
|
3482
|
+
dnsInstruction: result.dnsInstruction
|
|
3483
|
+
}, () => {
|
|
3484
|
+
success(`Domínio ${chalk.bold(dominio)} adicionado com sucesso!`);
|
|
3485
|
+
console.log();
|
|
3486
|
+
console.log(chalk.yellow.bold(" Instruções de DNS:"));
|
|
3487
|
+
console.log(chalk.white(` ${result.dnsInstruction}`));
|
|
3488
|
+
console.log();
|
|
3489
|
+
info(`Após configurar o DNS, execute: ${chalk.bold(`veloz domains verify ${result.id}`)}`);
|
|
3490
|
+
});
|
|
2706
3491
|
} catch (error) {
|
|
2707
3492
|
spin.stop();
|
|
2708
3493
|
handleError(error);
|
|
@@ -2713,13 +3498,19 @@ domainsCommand.command("verify <domainId>").alias("verificar").description("Veri
|
|
|
2713
3498
|
try {
|
|
2714
3499
|
const result = await (await getClient()).domains.verify({ domainId });
|
|
2715
3500
|
spin.stop();
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3501
|
+
output({
|
|
3502
|
+
type: "domain_verified",
|
|
3503
|
+
domain: result.domain.domain,
|
|
3504
|
+
verified: result.verified
|
|
3505
|
+
}, () => {
|
|
3506
|
+
if (result.verified) {
|
|
3507
|
+
success(`Domínio ${chalk.bold(result.domain.domain)} verificado com sucesso!`);
|
|
3508
|
+
info("O certificado TLS será provisionado automaticamente.");
|
|
3509
|
+
} else {
|
|
3510
|
+
console.log(chalk.yellow(`\n⚠ Domínio ${chalk.bold(result.domain.domain)} ainda não verificado.`));
|
|
3511
|
+
info("Verifique se o CNAME foi propagado e tente novamente.");
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
2723
3514
|
} catch (error) {
|
|
2724
3515
|
spin.stop();
|
|
2725
3516
|
handleError(error);
|
|
@@ -2762,14 +3553,22 @@ configCommand.command("show").description("Mostrar configurações atuais dos se
|
|
|
2762
3553
|
const client = await getClient();
|
|
2763
3554
|
const showHeaders = services.length > 1;
|
|
2764
3555
|
const spin = spinner("Buscando configurações...");
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
-
}
|
|
3556
|
+
const configs = [];
|
|
3557
|
+
for (const { service: svcConfig } of services) configs.push(await client.services.get({ serviceId: svcConfig.id }));
|
|
2772
3558
|
spin.stop();
|
|
3559
|
+
output({
|
|
3560
|
+
type: "service_configs",
|
|
3561
|
+
services: configs
|
|
3562
|
+
}, () => {
|
|
3563
|
+
for (let i = 0; i < services.length; i++) {
|
|
3564
|
+
const { service: svcConfig, index } = services[i];
|
|
3565
|
+
const service = configs[i];
|
|
3566
|
+
if (showHeaders) console.log(`\n${getServiceHeader(svcConfig.name, index)}\n`);
|
|
3567
|
+
else console.log(chalk.bold("\n📋 Configurações do Serviço\n"));
|
|
3568
|
+
printServiceConfig(service);
|
|
3569
|
+
console.log();
|
|
3570
|
+
}
|
|
3571
|
+
});
|
|
2773
3572
|
} catch (error) {
|
|
2774
3573
|
handleError(error);
|
|
2775
3574
|
}
|
|
@@ -2800,13 +3599,18 @@ configCommand.command("set").description("Atualizar configurações do serviço"
|
|
|
2800
3599
|
});
|
|
2801
3600
|
spin.stop();
|
|
2802
3601
|
success("Configurações atualizadas com sucesso!");
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
3602
|
+
output({
|
|
3603
|
+
type: "config_updated",
|
|
3604
|
+
updates
|
|
3605
|
+
}, () => {
|
|
3606
|
+
console.log(chalk.dim("\nValores atualizados:"));
|
|
3607
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
3608
|
+
const displayKey = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
|
|
3609
|
+
console.log(` ${chalk.bold(displayKey)}: ${formatValue(value)}`);
|
|
3610
|
+
}
|
|
3611
|
+
console.log();
|
|
3612
|
+
info("Execute 'veloz deploy' para aplicar as mudanças.");
|
|
3613
|
+
});
|
|
2810
3614
|
} catch (error) {
|
|
2811
3615
|
handleError(error);
|
|
2812
3616
|
}
|
|
@@ -2825,8 +3629,8 @@ configCommand.command("edit").description("Editar configurações interativament
|
|
|
2825
3629
|
if (name) updates.name = name;
|
|
2826
3630
|
const buildCmd = await prompt(`Build command ${chalk.dim(`(${service.buildCommand || "—"})`)}: `);
|
|
2827
3631
|
if (buildCmd) updates.buildCommand = buildCmd === "none" ? null : buildCmd;
|
|
2828
|
-
const startCmd
|
|
2829
|
-
if (startCmd
|
|
3632
|
+
const startCmd = await prompt(`Start command ${chalk.dim(`(${service.startCommand || "—"})`)}: `);
|
|
3633
|
+
if (startCmd) updates.startCommand = startCmd === "none" ? null : startCmd;
|
|
2830
3634
|
const port = await prompt(`Porta ${chalk.dim(`(${service.port})`)}: `);
|
|
2831
3635
|
if (port) updates.port = parseInt(port, 10);
|
|
2832
3636
|
const rootDir = await prompt(`Diretório raiz ${chalk.dim(`(${service.rootDirectory || "/"})`)}: `);
|
|
@@ -2888,15 +3692,9 @@ configCommand.command("reset").description("Resetar configurações para os padr
|
|
|
2888
3692
|
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
3693
|
try {
|
|
2890
3694
|
const config = loadConfig();
|
|
2891
|
-
if (!config)
|
|
2892
|
-
console.error(chalk.red("\n✗ Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
|
|
2893
|
-
process.exit(1);
|
|
2894
|
-
}
|
|
3695
|
+
if (!config) handleError(/* @__PURE__ */ new Error("Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
|
|
2895
3696
|
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
|
-
}
|
|
3697
|
+
if (services.length === 0) handleError(/* @__PURE__ */ new Error("Nenhum serviço encontrado na configuração."));
|
|
2900
3698
|
if (services.length === 1) {
|
|
2901
3699
|
info("Apenas um serviço configurado — já é o padrão.");
|
|
2902
3700
|
return;
|
|
@@ -2906,13 +3704,8 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
|
|
|
2906
3704
|
if (servicoArg) {
|
|
2907
3705
|
const found = services.find(([key, service]) => key === servicoArg || service.name.toLowerCase() === servicoArg.toLowerCase());
|
|
2908
3706
|
if (!found) {
|
|
2909
|
-
|
|
2910
|
-
|
|
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);
|
|
3707
|
+
const available = services.map(([key, svc]) => ` • ${key} (${svc.name})`).join("\n");
|
|
3708
|
+
handleError(/* @__PURE__ */ new Error(`Serviço '${servicoArg}' não encontrado.\n\nServiços disponíveis:\n${available}`));
|
|
2916
3709
|
}
|
|
2917
3710
|
selectedKey = found[0];
|
|
2918
3711
|
} else selectedKey = await promptSelect("Qual serviço usar como padrão?", services.map(([key, service]) => ({
|
|
@@ -2921,9 +3714,138 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
|
|
|
2921
3714
|
})));
|
|
2922
3715
|
setDefaultServiceKey(selectedKey);
|
|
2923
3716
|
const selectedService = config.services[selectedKey];
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3717
|
+
output({
|
|
3718
|
+
type: "default_service",
|
|
3719
|
+
key: selectedKey,
|
|
3720
|
+
name: selectedService.name
|
|
3721
|
+
}, () => {
|
|
3722
|
+
success(`Serviço padrão: ${chalk.bold(selectedService.name)} (${selectedKey})`);
|
|
3723
|
+
console.log(chalk.dim(`\nComandos como ${chalk.white("logs")}, ${chalk.white("env")}, ${chalk.white("config")} vão usar este serviço automaticamente.`));
|
|
3724
|
+
console.log(chalk.dim(`Use ${chalk.white("--service <nome>")} para sobrescrever pontualmente.`));
|
|
3725
|
+
});
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
handleError(error);
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
|
|
3731
|
+
//#endregion
|
|
3732
|
+
//#region src/commands/apikey.ts
|
|
3733
|
+
async function authFetch(path, options = {}) {
|
|
3734
|
+
const config = await requireAuth();
|
|
3735
|
+
const url = `${config.apiUrl}/api/auth${path}`;
|
|
3736
|
+
const res = await fetch(url, {
|
|
3737
|
+
...options,
|
|
3738
|
+
headers: {
|
|
3739
|
+
"Content-Type": "application/json",
|
|
3740
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
3741
|
+
...options.headers
|
|
3742
|
+
}
|
|
3743
|
+
});
|
|
3744
|
+
if (!res.ok) {
|
|
3745
|
+
const body = await res.text().catch(() => "");
|
|
3746
|
+
throw new Error(`Erro ${res.status}: ${body || res.statusText}`);
|
|
3747
|
+
}
|
|
3748
|
+
return res.json();
|
|
3749
|
+
}
|
|
3750
|
+
const apikeyCommand = new Command("apikey").description("Gerenciar chaves de API");
|
|
3751
|
+
apikeyCommand.command("create").description("Criar uma nova chave de API").option("--name <name>", "Nome da chave", "cli").option("--no-expire", "Chave sem expiração").action(async (opts) => {
|
|
3752
|
+
try {
|
|
3753
|
+
const s = spinner("Criando chave de API...");
|
|
3754
|
+
const body = {
|
|
3755
|
+
name: opts.name,
|
|
3756
|
+
prefix: "veloz"
|
|
3757
|
+
};
|
|
3758
|
+
if (opts.expire === false) body.expiresIn = null;
|
|
3759
|
+
const data = await authFetch("/api-key/create", {
|
|
3760
|
+
method: "POST",
|
|
3761
|
+
body: JSON.stringify(body)
|
|
3762
|
+
});
|
|
3763
|
+
s.stop();
|
|
3764
|
+
output({
|
|
3765
|
+
type: "apikey_created",
|
|
3766
|
+
key: data.key,
|
|
3767
|
+
name: data.name,
|
|
3768
|
+
expiresAt: data.expiresAt
|
|
3769
|
+
}, () => {
|
|
3770
|
+
success("Chave de API criada!");
|
|
3771
|
+
console.log();
|
|
3772
|
+
console.log(chalk.bold(" Chave:"), chalk.green(data.key));
|
|
3773
|
+
console.log(chalk.bold(" Nome:"), data.name);
|
|
3774
|
+
if (data.expiresAt) console.log(chalk.bold(" Expira:"), new Date(data.expiresAt).toLocaleDateString("pt-BR"));
|
|
3775
|
+
else console.log(chalk.bold(" Expira:"), "Nunca");
|
|
3776
|
+
console.log();
|
|
3777
|
+
console.log(chalk.yellow(" Guarde esta chave — ela não será exibida novamente."));
|
|
3778
|
+
console.log();
|
|
3779
|
+
console.log(chalk.dim(" Uso em CI:"));
|
|
3780
|
+
console.log(chalk.dim(` VELOZ_API_KEY=${data.key} veloz deploy -y --service web`));
|
|
3781
|
+
console.log();
|
|
3782
|
+
console.log(chalk.dim(" GitHub Actions:"));
|
|
3783
|
+
console.log(chalk.dim(` gh secret set VELOZ_API_KEY --body "${data.key}"`));
|
|
3784
|
+
console.log();
|
|
3785
|
+
});
|
|
3786
|
+
} catch (error) {
|
|
3787
|
+
handleError(error);
|
|
3788
|
+
}
|
|
3789
|
+
});
|
|
3790
|
+
apikeyCommand.command("list").alias("listar").description("Listar chaves de API").action(async () => {
|
|
3791
|
+
try {
|
|
3792
|
+
const s = spinner("Buscando chaves...");
|
|
3793
|
+
const data = await authFetch("/api-key/list", { method: "GET" });
|
|
3794
|
+
s.stop();
|
|
3795
|
+
const keys = Array.isArray(data) ? data : data.apiKeys ?? [];
|
|
3796
|
+
if (!keys || keys.length === 0) {
|
|
3797
|
+
info("Nenhuma chave de API encontrada.");
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
printTable([
|
|
3801
|
+
"Nome",
|
|
3802
|
+
"ID",
|
|
3803
|
+
"Criado",
|
|
3804
|
+
"Expira"
|
|
3805
|
+
], keys.map((k) => [
|
|
3806
|
+
k.name ?? "-",
|
|
3807
|
+
k.id,
|
|
3808
|
+
new Date(k.createdAt).toLocaleDateString("pt-BR"),
|
|
3809
|
+
k.expiresAt ? new Date(k.expiresAt).toLocaleDateString("pt-BR") : "Nunca"
|
|
3810
|
+
]), keys.map((k) => ({
|
|
3811
|
+
name: k.name,
|
|
3812
|
+
id: k.id,
|
|
3813
|
+
createdAt: k.createdAt,
|
|
3814
|
+
expiresAt: k.expiresAt
|
|
3815
|
+
})));
|
|
3816
|
+
} catch (error) {
|
|
3817
|
+
handleError(error);
|
|
3818
|
+
}
|
|
3819
|
+
});
|
|
3820
|
+
apikeyCommand.command("delete <keyId>").alias("deletar").description("Deletar uma chave de API").action(async (keyId) => {
|
|
3821
|
+
try {
|
|
3822
|
+
const s = spinner("Deletando chave...");
|
|
3823
|
+
await authFetch("/api-key/delete", {
|
|
3824
|
+
method: "POST",
|
|
3825
|
+
body: JSON.stringify({ keyId })
|
|
3826
|
+
});
|
|
3827
|
+
s.stop();
|
|
3828
|
+
success("Chave deletada.");
|
|
3829
|
+
} catch (error) {
|
|
3830
|
+
handleError(error);
|
|
3831
|
+
}
|
|
3832
|
+
});
|
|
3833
|
+
|
|
3834
|
+
//#endregion
|
|
3835
|
+
//#region src/commands/whoami.ts
|
|
3836
|
+
const whoamiCommand = new Command("whoami").description("Mostrar usuário autenticado").action(async () => {
|
|
3837
|
+
try {
|
|
3838
|
+
const user = await (await getClient()).me();
|
|
3839
|
+
output({
|
|
3840
|
+
type: "user",
|
|
3841
|
+
name: user.name,
|
|
3842
|
+
email: user.email
|
|
3843
|
+
}, () => {
|
|
3844
|
+
console.log();
|
|
3845
|
+
console.log(` ${chalk.bold("Nome:")} ${user.name}`);
|
|
3846
|
+
console.log(` ${chalk.bold("Email:")} ${user.email}`);
|
|
3847
|
+
console.log();
|
|
3848
|
+
});
|
|
2927
3849
|
} catch (error) {
|
|
2928
3850
|
handleError(error);
|
|
2929
3851
|
}
|
|
@@ -2931,8 +3853,23 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
|
|
|
2931
3853
|
|
|
2932
3854
|
//#endregion
|
|
2933
3855
|
//#region src/index.ts
|
|
2934
|
-
const
|
|
2935
|
-
const
|
|
3856
|
+
const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.10").option("--output <format>", "Formato de saída: fancy, json, github-actions, plain").option("--env <environment>", "Ambiente alvo (ex: preview, staging)").hook("preAction", (thisCommand) => {
|
|
3857
|
+
const opts = thisCommand.opts();
|
|
3858
|
+
if (opts.output) {
|
|
3859
|
+
const valid = [
|
|
3860
|
+
"fancy",
|
|
3861
|
+
"json",
|
|
3862
|
+
"github-actions",
|
|
3863
|
+
"plain"
|
|
3864
|
+
];
|
|
3865
|
+
if (!valid.includes(opts.output)) {
|
|
3866
|
+
console.error(`Formato de saída inválido: "${opts.output}". Use: ${valid.join(", ")}`);
|
|
3867
|
+
process.exit(1);
|
|
3868
|
+
}
|
|
3869
|
+
setOutputMode(opts.output);
|
|
3870
|
+
}
|
|
3871
|
+
if (opts.env) setActiveEnv(opts.env);
|
|
3872
|
+
});
|
|
2936
3873
|
program.addCommand(loginCommand);
|
|
2937
3874
|
program.addCommand(logoutCommand);
|
|
2938
3875
|
program.addCommand(projectsCommand);
|
|
@@ -2943,6 +3880,8 @@ program.addCommand(envCommand);
|
|
|
2943
3880
|
program.addCommand(domainsCommand);
|
|
2944
3881
|
program.addCommand(configCommand);
|
|
2945
3882
|
program.addCommand(useCommand);
|
|
3883
|
+
program.addCommand(apikeyCommand);
|
|
3884
|
+
program.addCommand(whoamiCommand);
|
|
2946
3885
|
program.parse();
|
|
2947
3886
|
|
|
2948
3887
|
//#endregion
|