onveloz 0.0.0-beta.1 → 0.0.0-beta.11
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 +2592 -1430
- 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}`));
|
|
121
492
|
}
|
|
122
|
-
function warn(message) {
|
|
123
|
-
|
|
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}`));
|
|
124
501
|
}
|
|
125
|
-
|
|
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();
|
|
514
|
+
}
|
|
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();
|
|
@@ -303,8 +789,14 @@ function createClient(baseUrl, headers) {
|
|
|
303
789
|
//#endregion
|
|
304
790
|
//#region src/lib/client.ts
|
|
305
791
|
async function getClient() {
|
|
306
|
-
const
|
|
307
|
-
|
|
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
|
+
});
|
|
308
800
|
}
|
|
309
801
|
|
|
310
802
|
//#endregion
|
|
@@ -326,622 +818,285 @@ projectsCommand.command("list").alias("listar").description("Listar todos os pro
|
|
|
326
818
|
"Repo GitHub",
|
|
327
819
|
"Criado em"
|
|
328
820
|
], projects.map((p) => [
|
|
329
|
-
p.id
|
|
821
|
+
p.id,
|
|
330
822
|
p.name,
|
|
331
823
|
p.slug,
|
|
332
824
|
p.githubRepoOwner && p.githubRepoName ? `${p.githubRepoOwner}/${p.githubRepoName}` : "—",
|
|
333
825
|
new Date(p.createdAt).toLocaleDateString("pt-BR")
|
|
334
|
-
]))
|
|
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
|
+
})));
|
|
335
833
|
} catch (error) {
|
|
336
834
|
spin.stop();
|
|
337
835
|
handleError(error);
|
|
338
836
|
}
|
|
339
837
|
});
|
|
340
838
|
|
|
341
|
-
//#endregion
|
|
342
|
-
//#region ../../packages/config/veloz-config.ts
|
|
343
|
-
const ServiceTypeSchema = z.enum(["web", "static"]);
|
|
344
|
-
const PackageManagerSchema = z.enum([
|
|
345
|
-
"npm",
|
|
346
|
-
"yarn",
|
|
347
|
-
"pnpm",
|
|
348
|
-
"bun",
|
|
349
|
-
"auto"
|
|
350
|
-
]);
|
|
351
|
-
const BuildConfigSchema = z.object({
|
|
352
|
-
command: z.string().nullable().optional(),
|
|
353
|
-
nodeVersion: z.string().regex(/^[0-9]+(\.x)?$/).default("20").optional(),
|
|
354
|
-
packageManager: PackageManagerSchema.default("auto").optional(),
|
|
355
|
-
installCommand: z.string().nullable().optional(),
|
|
356
|
-
outputDir: z.string().nullable().optional()
|
|
357
|
-
});
|
|
358
|
-
const RuntimeConfigSchema = z.object({
|
|
359
|
-
command: z.string().nullable().optional(),
|
|
360
|
-
port: z.number().min(1).max(65535).default(3e3).optional(),
|
|
361
|
-
healthCheck: z.object({
|
|
362
|
-
path: z.string().default("/").optional(),
|
|
363
|
-
interval: z.number().default(30).optional(),
|
|
364
|
-
timeout: z.number().default(10).optional()
|
|
365
|
-
}).optional()
|
|
366
|
-
});
|
|
367
|
-
const ResourcesSchema = z.object({
|
|
368
|
-
instances: z.number().min(1).max(10).default(1).optional(),
|
|
369
|
-
cpu: z.string().regex(/^[0-9]+(\.[0-9]+)?|[0-9]+m$/).default("500m").optional(),
|
|
370
|
-
memory: z.string().regex(/^[0-9]+(Mi|Gi)$/).default("512Mi").optional(),
|
|
371
|
-
autoscale: z.object({
|
|
372
|
-
enabled: z.boolean().default(false).optional(),
|
|
373
|
-
minInstances: z.number().min(1).default(1).optional(),
|
|
374
|
-
maxInstances: z.number().min(1).max(20).default(3).optional(),
|
|
375
|
-
targetCPU: z.number().min(10).max(90).default(70).optional()
|
|
376
|
-
}).optional()
|
|
377
|
-
});
|
|
378
|
-
const EnvVarDefinitionSchema = z.object({
|
|
379
|
-
description: z.string().optional(),
|
|
380
|
-
required: z.boolean().default(false).optional(),
|
|
381
|
-
example: z.string().optional()
|
|
382
|
-
});
|
|
383
|
-
const ServiceConfigSchema = z.object({
|
|
384
|
-
id: z.string(),
|
|
385
|
-
name: z.string(),
|
|
386
|
-
type: ServiceTypeSchema.default("web"),
|
|
387
|
-
root: z.string().default("."),
|
|
388
|
-
branch: z.string().optional(),
|
|
389
|
-
build: BuildConfigSchema.optional(),
|
|
390
|
-
runtime: RuntimeConfigSchema.optional(),
|
|
391
|
-
env: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
|
|
392
|
-
resources: ResourcesSchema.optional()
|
|
393
|
-
});
|
|
394
|
-
const ProjectConfigSchema = z.object({
|
|
395
|
-
id: z.string(),
|
|
396
|
-
name: z.string(),
|
|
397
|
-
slug: z.string().regex(/^[a-z0-9-]+$/).optional()
|
|
398
|
-
});
|
|
399
|
-
const ServiceDefaultsSchema = z.object({
|
|
400
|
-
type: ServiceTypeSchema.optional(),
|
|
401
|
-
branch: z.string().optional(),
|
|
402
|
-
build: BuildConfigSchema.optional(),
|
|
403
|
-
runtime: RuntimeConfigSchema.optional(),
|
|
404
|
-
resources: ResourcesSchema.optional()
|
|
405
|
-
});
|
|
406
|
-
const VelozConfigSchema = z.object({
|
|
407
|
-
$schema: z.string().optional(),
|
|
408
|
-
version: z.literal("1.0"),
|
|
409
|
-
project: ProjectConfigSchema,
|
|
410
|
-
services: z.record(z.string(), ServiceConfigSchema),
|
|
411
|
-
defaults: ServiceDefaultsSchema.optional(),
|
|
412
|
-
created: z.string().datetime().optional(),
|
|
413
|
-
updated: z.string().datetime().optional()
|
|
414
|
-
});
|
|
415
|
-
function mergeServiceWithDefaults(service, defaults) {
|
|
416
|
-
if (!defaults) return service;
|
|
417
|
-
return {
|
|
418
|
-
...service,
|
|
419
|
-
type: service.type ?? defaults.type ?? "web",
|
|
420
|
-
branch: service.branch ?? defaults.branch,
|
|
421
|
-
build: {
|
|
422
|
-
...defaults.build,
|
|
423
|
-
...service.build
|
|
424
|
-
},
|
|
425
|
-
runtime: {
|
|
426
|
-
...defaults.runtime,
|
|
427
|
-
...service.runtime
|
|
428
|
-
},
|
|
429
|
-
resources: {
|
|
430
|
-
...defaults.resources,
|
|
431
|
-
...service.resources
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function parseVelozConfig(data) {
|
|
436
|
-
return VelozConfigSchema.parse(data);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
//#endregion
|
|
440
|
-
//#region src/lib/link.ts
|
|
441
|
-
const CONFIG_FILE = "veloz.json";
|
|
442
|
-
const LOCAL_CONFIG_FILE = "veloz.local.json";
|
|
443
|
-
/**
|
|
444
|
-
* Find the root directory (git root or monorepo root)
|
|
445
|
-
*/
|
|
446
|
-
function findProjectRoot(startPath = process.cwd()) {
|
|
447
|
-
let currentPath = resolve(startPath);
|
|
448
|
-
const root = resolve("/");
|
|
449
|
-
while (currentPath !== root) {
|
|
450
|
-
if (existsSync(join(currentPath, ".git"))) return currentPath;
|
|
451
|
-
if (existsSync(join(currentPath, "pnpm-workspace.yaml")) || existsSync(join(currentPath, "lerna.json")) || existsSync(join(currentPath, "rush.json")) || existsSync(join(currentPath, "nx.json"))) return currentPath;
|
|
452
|
-
const pkgJsonPath = join(currentPath, "package.json");
|
|
453
|
-
if (existsSync(pkgJsonPath)) try {
|
|
454
|
-
if (JSON.parse(readFileSync(pkgJsonPath, "utf-8")).workspaces) return currentPath;
|
|
455
|
-
} catch {}
|
|
456
|
-
const parentPath = resolve(currentPath, "..");
|
|
457
|
-
if (parentPath === currentPath) break;
|
|
458
|
-
currentPath = parentPath;
|
|
459
|
-
}
|
|
460
|
-
return process.cwd();
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Get the config file name (veloz.local.json when VELOZ_API_URL is set, veloz.json otherwise)
|
|
464
|
-
*/
|
|
465
|
-
function getConfigFileName() {
|
|
466
|
-
return process.env.VELOZ_API_URL ? LOCAL_CONFIG_FILE : CONFIG_FILE;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Get the path to veloz.json (or veloz.local.json when using custom API URL)
|
|
470
|
-
*/
|
|
471
|
-
function getConfigPath() {
|
|
472
|
-
return join(findProjectRoot(), getConfigFileName());
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Load the veloz.json config from project root
|
|
476
|
-
*/
|
|
477
|
-
function loadConfig() {
|
|
478
|
-
const path = getConfigPath();
|
|
479
|
-
if (!existsSync(path)) return null;
|
|
480
|
-
try {
|
|
481
|
-
const raw = readFileSync(path, "utf-8");
|
|
482
|
-
return parseVelozConfig(JSON.parse(raw));
|
|
483
|
-
} catch (error) {
|
|
484
|
-
console.error(`Error loading ${getConfigFileName()}:`, error);
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* Save the veloz.json config to project root
|
|
490
|
-
*/
|
|
491
|
-
function saveConfig(config) {
|
|
492
|
-
const path = getConfigPath();
|
|
493
|
-
const configWithSchema = {
|
|
494
|
-
$schema: "https://veloz.app/schemas/veloz-config.schema.json",
|
|
495
|
-
...config
|
|
496
|
-
};
|
|
497
|
-
writeFileSync(path, JSON.stringify(configWithSchema, null, 2), "utf-8");
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Require config to exist, throw if not found
|
|
501
|
-
*/
|
|
502
|
-
function requireConfig() {
|
|
503
|
-
const config = loadConfig();
|
|
504
|
-
if (!config) throw new Error(`No ${getConfigFileName()} found in project root. Run 'veloz init' or 'veloz deploy' to set up your project.`);
|
|
505
|
-
return config;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Check if current directory is a git repository
|
|
509
|
-
*/
|
|
510
|
-
function isGitRepo() {
|
|
511
|
-
try {
|
|
512
|
-
execSync("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
513
|
-
return true;
|
|
514
|
-
} catch {
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Get git remote origin info
|
|
520
|
-
*/
|
|
521
|
-
function getGitRemote() {
|
|
522
|
-
try {
|
|
523
|
-
const url = execSync("git remote get-url origin", { stdio: "pipe" }).toString().trim();
|
|
524
|
-
const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
525
|
-
if (httpsMatch) return {
|
|
526
|
-
owner: httpsMatch[1],
|
|
527
|
-
repo: httpsMatch[2]
|
|
528
|
-
};
|
|
529
|
-
const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+)/);
|
|
530
|
-
if (sshMatch) return {
|
|
531
|
-
owner: sshMatch[1],
|
|
532
|
-
repo: sshMatch[2]
|
|
533
|
-
};
|
|
534
|
-
return null;
|
|
535
|
-
} catch {
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Get current git branch
|
|
541
|
-
*/
|
|
542
|
-
function getGitBranch() {
|
|
543
|
-
try {
|
|
544
|
-
return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
|
|
545
|
-
} catch {
|
|
546
|
-
return "main";
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
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]);
|
|
848
|
-
}
|
|
849
|
-
} else {
|
|
850
|
-
const pkgPath = base + "/package.json";
|
|
851
|
-
if (availableFiles.includes(pkgPath)) dirs.add(base);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
return [...dirs];
|
|
855
|
-
}
|
|
856
|
-
function detectMonorepo(files, pm = "npm") {
|
|
857
|
-
const rootPkg = safeParse(files["package.json"]);
|
|
858
|
-
const availableFiles = Object.keys(files);
|
|
859
|
-
let workspacePatterns = [];
|
|
860
|
-
if (rootPkg?.workspaces) {
|
|
861
|
-
const ws = rootPkg.workspaces;
|
|
862
|
-
if (Array.isArray(ws)) workspacePatterns = ws;
|
|
863
|
-
else if (typeof ws === "object" && ws !== null && "packages" in ws && Array.isArray(ws.packages)) workspacePatterns = ws.packages;
|
|
864
|
-
}
|
|
865
|
-
if (workspacePatterns.length === 0 && files["pnpm-workspace.yaml"]) {
|
|
866
|
-
const lines = files["pnpm-workspace.yaml"].split("\n");
|
|
867
|
-
let inPackages = false;
|
|
868
|
-
for (const line of lines) {
|
|
869
|
-
if (line.match(/^packages\s*:/)) {
|
|
870
|
-
inPackages = true;
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (inPackages) {
|
|
874
|
-
const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
|
|
875
|
-
if (match) workspacePatterns.push(match[1]);
|
|
876
|
-
else if (line.match(/^\S/)) break;
|
|
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
|
+
}
|
|
877
1054
|
}
|
|
878
1055
|
}
|
|
879
1056
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
`
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
}
|
|
1057
|
+
return vars;
|
|
1058
|
+
}
|
|
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,76 +1156,74 @@ 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
|
}
|
|
@@ -1085,86 +1238,26 @@ async function calculateDirectorySize(directory) {
|
|
|
1085
1238
|
}
|
|
1086
1239
|
|
|
1087
1240
|
//#endregion
|
|
1088
|
-
//#region src/lib/
|
|
1089
|
-
|
|
1090
|
-
QUEUED: "Na fila",
|
|
1091
|
-
BUILDING: "Construindo",
|
|
1092
|
-
BUILD_FAILED: "Falha na construção",
|
|
1093
|
-
DEPLOYING: "Implantando",
|
|
1094
|
-
LIVE: "Ativo",
|
|
1095
|
-
FAILED: "Falhou",
|
|
1096
|
-
CANCELLED: "Cancelado"
|
|
1097
|
-
};
|
|
1098
|
-
const TERMINAL_STATUSES$1 = new Set([
|
|
1099
|
-
"LIVE",
|
|
1100
|
-
"BUILD_FAILED",
|
|
1101
|
-
"FAILED",
|
|
1102
|
-
"CANCELLED"
|
|
1103
|
-
]);
|
|
1104
|
-
async function streamDeploymentLogs(client, deploymentId, _serviceId, serviceName) {
|
|
1105
|
-
const isVerbose = process.env.VELOZ_VERBOSE === "true";
|
|
1106
|
-
const logHeader = serviceName ? `📦 Build logs para ${chalk.bold(serviceName)}:` : "📦 Build logs:";
|
|
1107
|
-
console.log(chalk.cyan(`\n${logHeader}`));
|
|
1108
|
-
console.log(chalk.dim("─".repeat(60)));
|
|
1109
|
-
if (isVerbose) console.log(chalk.dim(`[verbose] Conectando ao stream de logs para deployment ${deploymentId}...`));
|
|
1110
|
-
let finalStatus = "";
|
|
1111
|
-
let logsReceived = false;
|
|
1112
|
-
try {
|
|
1113
|
-
const stream = await client.logs.streamBuildLogs({ deploymentId });
|
|
1114
|
-
for await (const event of stream) {
|
|
1115
|
-
logsReceived = true;
|
|
1116
|
-
if (event.type === "status") {
|
|
1117
|
-
const label = statusLabels$1[event.content] ?? event.content;
|
|
1118
|
-
const icon = event.content === "LIVE" ? chalk.green("●") : event.content === "BUILD_FAILED" || event.content === "FAILED" ? chalk.red("●") : chalk.yellow("●");
|
|
1119
|
-
process.stdout.write(`\n${icon} ${chalk.bold(label)}\n`);
|
|
1120
|
-
finalStatus = event.content;
|
|
1121
|
-
if (isVerbose) console.log(chalk.dim(`[verbose] Status mudou para: ${event.content}`));
|
|
1122
|
-
} else if (event.type === "log") process.stdout.write(event.content);
|
|
1123
|
-
}
|
|
1124
|
-
if (!logsReceived && isVerbose) console.log(chalk.yellow("[verbose] Nenhum log recebido do stream. Buscando status do deployment..."));
|
|
1125
|
-
} catch (error) {
|
|
1126
|
-
if (isVerbose) console.log(chalk.red(`[verbose] Erro no stream: ${error.message}`));
|
|
1127
|
-
try {
|
|
1128
|
-
const d = await client.deployments.get({ deploymentId });
|
|
1129
|
-
finalStatus = d.status;
|
|
1130
|
-
if (isVerbose) console.log(chalk.dim(`[verbose] Status do deployment: ${d.status}`));
|
|
1131
|
-
try {
|
|
1132
|
-
const logs = await client.logs.getBuildLogs({ deploymentId });
|
|
1133
|
-
if (logs.buildLogs) {
|
|
1134
|
-
console.log(chalk.dim("\n── Logs salvos ──\n"));
|
|
1135
|
-
process.stdout.write(logs.buildLogs);
|
|
1136
|
-
} else if (isVerbose) console.log(chalk.yellow("[verbose] Nenhum log salvo encontrado para este deployment"));
|
|
1137
|
-
} catch {
|
|
1138
|
-
if (isVerbose) console.log(chalk.yellow("[verbose] Não foi possível buscar logs de build"));
|
|
1139
|
-
}
|
|
1140
|
-
} catch (fetchError) {
|
|
1141
|
-
if (isVerbose) console.log(chalk.red(`[verbose] Erro ao buscar deployment: ${fetchError.message}`));
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
console.log(chalk.dim("\n" + "─".repeat(60)));
|
|
1145
|
-
if (finalStatus === "LIVE") success(serviceName ? `Deploy de ${chalk.bold(serviceName)} concluído! Serviço está ativo.` : "Deploy concluído! Serviço está ativo.");
|
|
1146
|
-
else if (TERMINAL_STATUSES$1.has(finalStatus)) {
|
|
1147
|
-
const errorMsg = serviceName ? `✗ Deploy de ${chalk.bold(serviceName)} finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}` : `✗ Deploy finalizou com status: ${statusLabels$1[finalStatus] ?? finalStatus}`;
|
|
1148
|
-
console.error(chalk.red(errorMsg));
|
|
1149
|
-
if (!serviceName) process.exit(1);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
//#endregion
|
|
1154
|
-
//#region src/lib/deploy-parallel.ts
|
|
1155
|
-
async function withRetry$1(fn, maxRetries = 3) {
|
|
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)}`);
|
|
1316
1564
|
}
|
|
1317
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}`));
|
|
1593
|
+
}
|
|
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
|
-
case "npm": return "";
|
|
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 [];
|
|
1330
1607
|
}
|
|
1331
1608
|
}
|
|
1332
|
-
function
|
|
1333
|
-
switch (
|
|
1334
|
-
case "
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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)));
|
|
1645
|
+
}
|
|
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
|
-
|
|
1428
|
-
# Copy full source
|
|
1429
|
-
COPY . .
|
|
1430
|
-
|
|
1431
|
-
# Build with env vars from BuildKit secret mount
|
|
1432
|
-
RUN --mount=type=secret,id=build-env \\
|
|
1433
|
-
set -a && \\
|
|
1434
|
-
if [ -f /run/secrets/build-env ]; then . /run/secrets/build-env; fi && \\
|
|
1435
|
-
set +a && \\
|
|
1436
|
-
${workdirPrefix}${buildCommand}
|
|
1437
|
-
|
|
1438
|
-
# Detect output directory
|
|
1439
|
-
${outputDetection}
|
|
1440
|
-
|
|
1441
|
-
# Process _headers and _redirects into nginx config
|
|
1442
|
-
COPY nginx.conf /tmp/nginx-base.conf
|
|
1443
|
-
COPY generate-nginx-config.cjs /tmp/generate-nginx-config.cjs
|
|
1444
|
-
RUN node /tmp/generate-nginx-config.cjs \\
|
|
1445
|
-
--base /tmp/nginx-base.conf \\
|
|
1446
|
-
--output-dir /app/${outputDirRef} \\
|
|
1447
|
-
--out /tmp/nginx-final.conf
|
|
1448
|
-
|
|
1449
|
-
# Copy build output to a known location
|
|
1450
|
-
RUN cp -r /app/${outputDirRef} /output
|
|
1451
|
-
|
|
1452
|
-
# ── Stage 2: Serve ─────────────────────────────────────
|
|
1453
|
-
FROM nginx:alpine
|
|
1454
|
-
|
|
1455
|
-
# Remove default nginx site
|
|
1456
|
-
RUN rm -f /etc/nginx/conf.d/default.conf
|
|
1457
|
-
|
|
1458
|
-
# Copy processed nginx config and static files
|
|
1459
|
-
COPY --from=builder /tmp/nginx-final.conf /etc/nginx/conf.d/default.conf
|
|
1460
|
-
COPY --from=builder /output /usr/share/nginx/html
|
|
1461
1766
|
|
|
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.11";
|
|
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
|
+
}
|
|
1469
2002
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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;
|
|
1473
2021
|
}
|
|
1474
|
-
|
|
1475
|
-
//#endregion
|
|
1476
|
-
//#region src/lib/templates.ts
|
|
1477
2022
|
/**
|
|
1478
|
-
*
|
|
1479
|
-
* These are written to the project directory temporarily before creating the tar,
|
|
1480
|
-
* then cleaned up immediately after.
|
|
2023
|
+
* Check for .env files that might be accidentally uploaded.
|
|
1481
2024
|
*/
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
gzip on;
|
|
1495
|
-
gzip_vary on;
|
|
1496
|
-
gzip_min_length 256;
|
|
1497
|
-
gzip_types
|
|
1498
|
-
text/plain
|
|
1499
|
-
text/css
|
|
1500
|
-
text/xml
|
|
1501
|
-
text/javascript
|
|
1502
|
-
application/json
|
|
1503
|
-
application/javascript
|
|
1504
|
-
application/xml+rss
|
|
1505
|
-
application/rss+xml
|
|
1506
|
-
application/atom+xml
|
|
1507
|
-
image/svg+xml
|
|
1508
|
-
text/x-component
|
|
1509
|
-
text/x-cross-domain-policy;
|
|
1510
|
-
|
|
1511
|
-
# Cache hashed assets (fingerprinted files)
|
|
1512
|
-
location ~* \\\\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf|webp|avif)\\$ {
|
|
1513
|
-
expires 1y;
|
|
1514
|
-
add_header Cache-Control "public, immutable";
|
|
1515
|
-
access_log off;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
# No cache for HTML files
|
|
1519
|
-
location ~* \\\\.html\\$ {
|
|
1520
|
-
expires -1;
|
|
1521
|
-
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
1522
|
-
add_header Pragma "no-cache";
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
# Security headers
|
|
1526
|
-
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
1527
|
-
add_header X-Content-Type-Options "nosniff" always;
|
|
1528
|
-
add_header X-XSS-Protection "1; mode=block" always;
|
|
1529
|
-
|
|
1530
|
-
# Custom headers placeholder (will be replaced during build)
|
|
1531
|
-
# CUSTOM_HEADERS_PLACEHOLDER
|
|
1532
|
-
|
|
1533
|
-
# Custom redirects placeholder (will be replaced during build)
|
|
1534
|
-
# CUSTOM_REDIRECTS_PLACEHOLDER
|
|
1535
|
-
}`;
|
|
1536
|
-
const GENERATE_NGINX_CONFIG_CJS = `#!/usr/bin/env node
|
|
1537
|
-
"use strict";
|
|
1538
|
-
|
|
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
|
+
}
|
|
1539
2037
|
/**
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
+
}
|
|
1560
2083
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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;
|
|
1567
2111
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const
|
|
1580
|
-
if (
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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");
|
|
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;
|
|
1629
2133
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
const from = parts[0];
|
|
1650
|
-
const to = parts[1];
|
|
1651
|
-
const code = parts[2] || "301";
|
|
1652
|
-
|
|
1653
|
-
if (!from || !to) continue;
|
|
1654
|
-
|
|
1655
|
-
if (!from.includes("*") && !to.includes(":splat")) {
|
|
1656
|
-
// Simple redirect
|
|
1657
|
-
const nginxFlag = code === "301" ? "permanent" : "redirect";
|
|
1658
|
-
redirectDirectives += \\\` rewrite ^\\\${from}\\\\$ \\\${to} \\\${nginxFlag};\\n\\\`;
|
|
1659
|
-
} else {
|
|
1660
|
-
// Wildcard redirect
|
|
1661
|
-
const fromPattern = from.replace(/\\*/g, "(.*)");
|
|
1662
|
-
const toPattern = to.replace(/:splat/g, "\\$1");
|
|
1663
|
-
const nginxFlag = code === "301" ? "permanent" : "redirect";
|
|
1664
|
-
redirectDirectives += \\\` rewrite ^\\\${fromPattern}\\\\$ \\\${toPattern} \\\${nginxFlag};\\n\\\`;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
nginxConfig = nginxConfig.replace(
|
|
1669
|
-
"# CUSTOM_REDIRECTS_PLACEHOLDER",
|
|
1670
|
-
redirectDirectives
|
|
1671
|
-
);
|
|
1672
|
-
console.log("Processed _redirects file");
|
|
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;
|
|
1673
2152
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
const
|
|
1688
|
-
if (
|
|
1689
|
-
|
|
1690
|
-
|
|
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 {
|
|
1691
2173
|
continue;
|
|
1692
2174
|
}
|
|
1693
|
-
throw error;
|
|
1694
2175
|
}
|
|
1695
|
-
|
|
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;
|
|
2192
|
+
}
|
|
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;
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Run all pre-deploy checks and return warnings.
|
|
2229
|
+
* Does not block — just warns the user.
|
|
2230
|
+
*/
|
|
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;
|
|
1696
2253
|
}
|
|
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;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
//#endregion
|
|
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.11";
|
|
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 = {};
|
|
@@ -1912,92 +2521,392 @@ function detectLocalRepo(basePath = ".") {
|
|
|
1912
2521
|
if (nestedPkg) files[`${base}/package.json`] = nestedPkg;
|
|
1913
2522
|
}
|
|
1914
2523
|
}
|
|
1915
|
-
return analyzeRepo(files);
|
|
2524
|
+
return analyzeRepo(files);
|
|
2525
|
+
}
|
|
2526
|
+
async function promptEnvVars(serviceId, detectedVars) {
|
|
2527
|
+
if (detectedVars.length === 0) return;
|
|
2528
|
+
console.log(chalk.cyan(`\n📝 ${detectedVars.length} variável(is) de ambiente detectada(s):\n`));
|
|
2529
|
+
for (const v of detectedVars) console.log(` • ${v}`);
|
|
2530
|
+
console.log();
|
|
2531
|
+
if (!await promptConfirm("Deseja preencher as variáveis agora?", false)) return;
|
|
2532
|
+
const vars = {};
|
|
2533
|
+
for (const key of detectedVars) {
|
|
2534
|
+
const val = await prompt(` ${chalk.bold(key)}:`);
|
|
2535
|
+
if (val) vars[key] = val;
|
|
2536
|
+
}
|
|
2537
|
+
const filled = Object.keys(vars).length;
|
|
2538
|
+
if (filled > 0) {
|
|
2539
|
+
const spinVars = spinner("Definindo variáveis de ambiente...");
|
|
2540
|
+
const client = await getClient();
|
|
2541
|
+
await withRetry(() => client.envVars.setBulk({
|
|
2542
|
+
serviceId,
|
|
2543
|
+
vars
|
|
2544
|
+
}));
|
|
2545
|
+
spinVars.stop();
|
|
2546
|
+
success(`${filled} variável(is) definida(s).`);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
2550
|
+
const client = await getClient();
|
|
2551
|
+
const branch = getGitBranch();
|
|
2552
|
+
const spinDetect = spinner("Detectando framework...");
|
|
2553
|
+
const detection = detectLocalRepo();
|
|
2554
|
+
spinDetect.stop();
|
|
2555
|
+
const pm = detection.packageManager;
|
|
2556
|
+
if (detection.isMonorepo && detection.monorepoApps.length > 0) {
|
|
2557
|
+
info(`Monorepo detectado (${pm})`);
|
|
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
|
+
}
|
|
2588
|
+
for (const app of selectedApps) {
|
|
2589
|
+
console.log(chalk.cyan(`\n── ${app.name} ──`));
|
|
2590
|
+
printSummary({
|
|
2591
|
+
name: app.name,
|
|
2592
|
+
type: "WEB",
|
|
2593
|
+
rootDir: app.root,
|
|
2594
|
+
branch,
|
|
2595
|
+
framework: app.framework,
|
|
2596
|
+
packageManager: pm,
|
|
2597
|
+
buildCommand: app.buildCommand,
|
|
2598
|
+
startCommand: app.startCommand,
|
|
2599
|
+
outputDir: null,
|
|
2600
|
+
port: app.port ?? 3e3
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
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
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const config = {
|
|
2615
|
+
version: "1.0",
|
|
2616
|
+
project: {
|
|
2617
|
+
id: projectId,
|
|
2618
|
+
name: projectName
|
|
2619
|
+
},
|
|
2620
|
+
services: {},
|
|
2621
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2622
|
+
};
|
|
2623
|
+
const createdServices = [];
|
|
2624
|
+
for (const app of selectedApps) {
|
|
2625
|
+
console.log(chalk.cyan(`\n── Criando serviço: ${app.name} ──\n`));
|
|
2626
|
+
const spinService$1 = spinner(`Criando serviço ${chalk.bold(app.name)}...`);
|
|
2627
|
+
const service$1 = await withRetry(() => client.services.create({
|
|
2628
|
+
projectId,
|
|
2629
|
+
name: app.name,
|
|
2630
|
+
type: "WEB",
|
|
2631
|
+
branch,
|
|
2632
|
+
rootDirectory: app.root,
|
|
2633
|
+
buildCommand: app.buildCommand ?? void 0,
|
|
2634
|
+
startCommand: app.startCommand ?? void 0,
|
|
2635
|
+
port: app.port ?? 3e3
|
|
2636
|
+
}));
|
|
2637
|
+
spinService$1.stop();
|
|
2638
|
+
success(`Serviço criado: ${chalk.bold(service$1.name)}`);
|
|
2639
|
+
createdServices.push({
|
|
2640
|
+
service: service$1,
|
|
2641
|
+
app
|
|
2642
|
+
});
|
|
2643
|
+
config.services[app.root] = {
|
|
2644
|
+
id: service$1.id,
|
|
2645
|
+
name: service$1.name,
|
|
2646
|
+
type: "web",
|
|
2647
|
+
root: app.root,
|
|
2648
|
+
branch,
|
|
2649
|
+
build: app.buildCommand ? { command: app.buildCommand ?? void 0 } : void 0,
|
|
2650
|
+
runtime: {
|
|
2651
|
+
command: app.startCommand ?? void 0,
|
|
2652
|
+
port: app.port ?? 3e3
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
saveConfig(config);
|
|
2657
|
+
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2658
|
+
if (!opts.yes) for (const { service: service$1, app } of createdServices) {
|
|
2659
|
+
console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
|
|
2660
|
+
const serviceDetection = detectLocalRepo(app.root);
|
|
2661
|
+
await promptEnvVars(service$1.id, serviceDetection.envVars.map((v) => v.key));
|
|
2662
|
+
}
|
|
2663
|
+
await deployServicesInParallel(createdServices.map(({ service: service$1, app }) => ({
|
|
2664
|
+
serviceId: service$1.id,
|
|
2665
|
+
serviceName: app.name,
|
|
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
|
|
2673
|
+
})
|
|
2674
|
+
})));
|
|
2675
|
+
return createdServices[createdServices.length - 1]?.service.id || "";
|
|
2676
|
+
}
|
|
2677
|
+
const fw = detection.framework;
|
|
2678
|
+
const settings = {
|
|
2679
|
+
name: repoName,
|
|
2680
|
+
type: fw?.type ?? "WEB",
|
|
2681
|
+
rootDir: ".",
|
|
2682
|
+
branch,
|
|
2683
|
+
framework: fw?.name ?? null,
|
|
2684
|
+
packageManager: pm,
|
|
2685
|
+
buildCommand: fw?.buildCommand ?? null,
|
|
2686
|
+
startCommand: fw?.startCommand ?? null,
|
|
2687
|
+
outputDir: fw?.outputDir ?? null,
|
|
2688
|
+
port: fw?.port ?? 3e3
|
|
2689
|
+
};
|
|
2690
|
+
if (fw) info(`Framework detectado: ${chalk.bold(fw.name)}`);
|
|
2691
|
+
printSummary(settings);
|
|
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
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
const spinService = spinner("Criando serviço...");
|
|
2712
|
+
const service = await withRetry(() => client.services.create({
|
|
2713
|
+
projectId,
|
|
2714
|
+
name: settings.name,
|
|
2715
|
+
type: settings.type,
|
|
2716
|
+
branch,
|
|
2717
|
+
rootDirectory: settings.rootDir,
|
|
2718
|
+
buildCommand: settings.buildCommand ?? void 0,
|
|
2719
|
+
startCommand: settings.startCommand ?? void 0,
|
|
2720
|
+
port: settings.port
|
|
2721
|
+
}));
|
|
2722
|
+
spinService.stop();
|
|
2723
|
+
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2724
|
+
if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key));
|
|
2725
|
+
saveConfig({
|
|
2726
|
+
version: "1.0",
|
|
2727
|
+
project: {
|
|
2728
|
+
id: projectId,
|
|
2729
|
+
name: projectName
|
|
2730
|
+
},
|
|
2731
|
+
services: { main: {
|
|
2732
|
+
id: service.id,
|
|
2733
|
+
name: service.name,
|
|
2734
|
+
type: settings.type.toLowerCase(),
|
|
2735
|
+
root: settings.rootDir,
|
|
2736
|
+
branch,
|
|
2737
|
+
build: settings.buildCommand || settings.outputDir ? {
|
|
2738
|
+
command: settings.buildCommand ?? void 0,
|
|
2739
|
+
outputDir: settings.outputDir ?? void 0
|
|
2740
|
+
} : void 0,
|
|
2741
|
+
runtime: {
|
|
2742
|
+
command: settings.startCommand ?? void 0,
|
|
2743
|
+
port: settings.port
|
|
2744
|
+
}
|
|
2745
|
+
} },
|
|
2746
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2747
|
+
});
|
|
2748
|
+
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2749
|
+
return service.id;
|
|
1916
2750
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
if (
|
|
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
|
+
}
|
|
1927
2764
|
}
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
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
|
|
1934
2787
|
}));
|
|
1935
|
-
|
|
1936
|
-
success(
|
|
2788
|
+
spinService.stop();
|
|
2789
|
+
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2790
|
+
envServices[key] = {
|
|
2791
|
+
id: service.id,
|
|
2792
|
+
name: serviceName
|
|
2793
|
+
};
|
|
1937
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();
|
|
1938
2813
|
}
|
|
1939
|
-
|
|
2814
|
+
/**
|
|
2815
|
+
* Add a new service to an existing project that already has a veloz.json config.
|
|
2816
|
+
* For monorepos, filters out already-configured apps and lets the user pick from the rest.
|
|
2817
|
+
* For single repos, prompts for a new service name and config.
|
|
2818
|
+
*/
|
|
2819
|
+
async function addServiceFlow(existingConfig, opts) {
|
|
2820
|
+
const client = await getClient();
|
|
1940
2821
|
const branch = getGitBranch();
|
|
2822
|
+
const projectId = existingConfig.project.id;
|
|
1941
2823
|
const spinDetect = spinner("Detectando framework...");
|
|
1942
2824
|
const detection = detectLocalRepo();
|
|
1943
2825
|
spinDetect.stop();
|
|
1944
2826
|
const pm = detection.packageManager;
|
|
2827
|
+
const existingRoots = new Set(Object.values(existingConfig.services).map((s) => s.root));
|
|
1945
2828
|
if (detection.isMonorepo && detection.monorepoApps.length > 0) {
|
|
1946
2829
|
info(`Monorepo detectado (${pm})`);
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2830
|
+
const availableApps = detection.monorepoApps.map((a) => ({
|
|
2831
|
+
name: a.name,
|
|
2832
|
+
root: a.path,
|
|
2833
|
+
framework: a.framework?.name ?? null,
|
|
2834
|
+
buildCommand: a.framework?.buildCommand ?? null,
|
|
2835
|
+
startCommand: a.framework?.startCommand ?? null,
|
|
2836
|
+
port: a.framework?.port ?? 3e3
|
|
2837
|
+
})).filter((a) => !existingRoots.has(a.root));
|
|
2838
|
+
if (availableApps.length === 0) {
|
|
2839
|
+
info("Todos os apps do monorepo já estão configurados.");
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
let selectedApps;
|
|
2843
|
+
if (opts.yes) if (opts.app) {
|
|
2844
|
+
selectedApps = availableApps.filter((a) => a.root === opts.app);
|
|
2845
|
+
if (selectedApps.length === 0) {
|
|
2846
|
+
output({
|
|
2847
|
+
type: "error",
|
|
2848
|
+
message: `App '${opts.app}' não encontrado ou já configurado.`,
|
|
2849
|
+
available: availableApps.map((a) => a.root)
|
|
2850
|
+
}, () => {
|
|
2851
|
+
const available = availableApps.map((a) => ` • ${a.root}`).join("\n");
|
|
2852
|
+
console.error(chalk.red(`\n✗ App '${opts.app}' não encontrado ou já configurado.\n\nApps disponíveis:\n${available}`));
|
|
2853
|
+
});
|
|
2854
|
+
process.exit(1);
|
|
2855
|
+
}
|
|
2856
|
+
} else selectedApps = availableApps;
|
|
2857
|
+
else {
|
|
2858
|
+
const selectedPaths = await promptMultiSelect("Quais apps deseja adicionar?", availableApps.map((app) => ({
|
|
2859
|
+
label: `${app.name}${app.framework ? ` (${app.framework})` : ""} — ${app.root}`,
|
|
2860
|
+
value: app.root
|
|
2861
|
+
})));
|
|
2862
|
+
selectedApps = availableApps.filter((a) => selectedPaths.includes(a.root));
|
|
2863
|
+
}
|
|
2864
|
+
if (selectedApps.length === 0) {
|
|
2865
|
+
info("Nenhum app selecionado.");
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
1952
2868
|
for (const app of selectedApps) {
|
|
1953
|
-
const fw$1 = app.framework;
|
|
1954
2869
|
console.log(chalk.cyan(`\n── ${app.name} ──`));
|
|
1955
2870
|
printSummary({
|
|
1956
2871
|
name: app.name,
|
|
1957
|
-
type:
|
|
1958
|
-
rootDir: app.
|
|
2872
|
+
type: "WEB",
|
|
2873
|
+
rootDir: app.root,
|
|
1959
2874
|
branch,
|
|
1960
|
-
framework:
|
|
2875
|
+
framework: app.framework,
|
|
1961
2876
|
packageManager: pm,
|
|
1962
|
-
buildCommand:
|
|
1963
|
-
startCommand:
|
|
1964
|
-
outputDir:
|
|
1965
|
-
port:
|
|
2877
|
+
buildCommand: app.buildCommand,
|
|
2878
|
+
startCommand: app.startCommand,
|
|
2879
|
+
outputDir: null,
|
|
2880
|
+
port: app.port ?? 3e3
|
|
1966
2881
|
});
|
|
1967
2882
|
}
|
|
1968
|
-
if (!
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2883
|
+
if (!opts.yes) {
|
|
2884
|
+
if (!await promptConfirm("Confirmar e fazer deploy?")) for (const app of selectedApps) {
|
|
2885
|
+
console.log(chalk.cyan(`\n── Editar: ${app.name} ──`));
|
|
2886
|
+
const newBuild = await prompt(` Build command: ${chalk.dim(`(${app.buildCommand ?? "—"})`)}`);
|
|
2887
|
+
if (newBuild) app.buildCommand = newBuild;
|
|
2888
|
+
const newStart = await prompt(` Start command: ${chalk.dim(`(${app.startCommand ?? "—"})`)}`);
|
|
2889
|
+
if (newStart) app.startCommand = newStart;
|
|
2890
|
+
const newPort = await prompt(` Port: ${chalk.dim(`(${app.port ?? 3e3})`)}`);
|
|
2891
|
+
if (newPort) app.port = parseInt(newPort, 10) || app.port;
|
|
2892
|
+
}
|
|
1977
2893
|
}
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
id: projectId,
|
|
1982
|
-
name: projectName
|
|
1983
|
-
},
|
|
1984
|
-
services: {},
|
|
1985
|
-
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2894
|
+
const updatedConfig = {
|
|
2895
|
+
...existingConfig,
|
|
2896
|
+
services: { ...existingConfig.services }
|
|
1986
2897
|
};
|
|
1987
2898
|
const createdServices = [];
|
|
1988
2899
|
for (const app of selectedApps) {
|
|
1989
|
-
const fw$1 = app.framework;
|
|
1990
|
-
console.log(chalk.cyan(`\n── Criando serviço: ${app.name} ──\n`));
|
|
1991
2900
|
const spinService$1 = spinner(`Criando serviço ${chalk.bold(app.name)}...`);
|
|
1992
2901
|
const service$1 = await withRetry(() => client.services.create({
|
|
1993
2902
|
projectId,
|
|
1994
2903
|
name: app.name,
|
|
1995
|
-
type:
|
|
2904
|
+
type: "WEB",
|
|
1996
2905
|
branch,
|
|
1997
|
-
rootDirectory: app.
|
|
1998
|
-
buildCommand:
|
|
1999
|
-
startCommand:
|
|
2000
|
-
port:
|
|
2906
|
+
rootDirectory: app.root,
|
|
2907
|
+
buildCommand: app.buildCommand ?? void 0,
|
|
2908
|
+
startCommand: app.startCommand ?? void 0,
|
|
2909
|
+
port: app.port ?? 3e3
|
|
2001
2910
|
}));
|
|
2002
2911
|
spinService$1.stop();
|
|
2003
2912
|
success(`Serviço criado: ${chalk.bold(service$1.name)}`);
|
|
@@ -2005,60 +2914,56 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2005
2914
|
service: service$1,
|
|
2006
2915
|
app
|
|
2007
2916
|
});
|
|
2008
|
-
|
|
2917
|
+
updatedConfig.services[app.root] = {
|
|
2009
2918
|
id: service$1.id,
|
|
2010
2919
|
name: service$1.name,
|
|
2011
|
-
type:
|
|
2012
|
-
root: app.
|
|
2920
|
+
type: "web",
|
|
2921
|
+
root: app.root,
|
|
2013
2922
|
branch,
|
|
2014
|
-
build:
|
|
2015
|
-
command: fw$1?.buildCommand ?? void 0,
|
|
2016
|
-
outputDir: fw$1?.outputDir ?? void 0
|
|
2017
|
-
} : void 0,
|
|
2923
|
+
build: app.buildCommand ? { command: app.buildCommand ?? void 0 } : void 0,
|
|
2018
2924
|
runtime: {
|
|
2019
|
-
command:
|
|
2020
|
-
port:
|
|
2925
|
+
command: app.startCommand ?? void 0,
|
|
2926
|
+
port: app.port ?? 3e3
|
|
2021
2927
|
}
|
|
2022
2928
|
};
|
|
2023
2929
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2930
|
+
updatedConfig.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2931
|
+
saveConfig(updatedConfig);
|
|
2932
|
+
info(`Arquivo ${getConfigFileName()} atualizado com ${createdServices.length} novo(s) serviço(s).`);
|
|
2933
|
+
if (!opts.yes) for (const { service: service$1, app } of createdServices) {
|
|
2027
2934
|
console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
|
|
2028
|
-
const serviceDetection = detectLocalRepo(app.
|
|
2029
|
-
await promptEnvVars(
|
|
2935
|
+
const serviceDetection = detectLocalRepo(app.root);
|
|
2936
|
+
await promptEnvVars(service$1.id, serviceDetection.envVars.map((v) => v.key));
|
|
2030
2937
|
}
|
|
2031
|
-
await deployServicesInParallel(
|
|
2938
|
+
await deployServicesInParallel(createdServices.map(({ service: service$1, app }) => ({
|
|
2032
2939
|
serviceId: service$1.id,
|
|
2033
2940
|
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
|
|
2941
|
+
path: resolve(process.cwd(), app.root),
|
|
2942
|
+
extraFiles: prepareExtraFiles(detectLocalRepo(app.root), {
|
|
2943
|
+
type: "WEB",
|
|
2944
|
+
buildCommand: app.buildCommand ?? void 0,
|
|
2945
|
+
startCommand: app.startCommand ?? void 0,
|
|
2946
|
+
port: app.port ?? void 0,
|
|
2947
|
+
rootDirectory: app.root
|
|
2042
2948
|
})
|
|
2043
2949
|
})));
|
|
2044
|
-
return
|
|
2950
|
+
return;
|
|
2045
2951
|
}
|
|
2046
2952
|
const fw = detection.framework;
|
|
2047
2953
|
const settings = {
|
|
2048
|
-
name:
|
|
2954
|
+
name: existingConfig.project.name + "-new",
|
|
2049
2955
|
type: fw?.type ?? "WEB",
|
|
2050
2956
|
rootDir: ".",
|
|
2051
2957
|
branch,
|
|
2052
|
-
framework: fw?.
|
|
2958
|
+
framework: fw?.name ?? null,
|
|
2053
2959
|
packageManager: pm,
|
|
2054
2960
|
buildCommand: fw?.buildCommand ?? null,
|
|
2055
2961
|
startCommand: fw?.startCommand ?? null,
|
|
2056
2962
|
outputDir: fw?.outputDir ?? null,
|
|
2057
2963
|
port: fw?.port ?? 3e3
|
|
2058
2964
|
};
|
|
2059
|
-
if (fw) info(`Framework detectado: ${chalk.bold(fw.
|
|
2060
|
-
|
|
2061
|
-
if (!await promptConfirm("Confirmar e fazer deploy?")) {
|
|
2965
|
+
if (fw) info(`Framework detectado: ${chalk.bold(fw.name)}`);
|
|
2966
|
+
if (!opts.yes) {
|
|
2062
2967
|
const newName = await prompt(`Nome do serviço: ${chalk.dim(`(${settings.name})`)}`);
|
|
2063
2968
|
if (newName) settings.name = newName;
|
|
2064
2969
|
settings.type = await promptSelect("Tipo de serviço:", [{
|
|
@@ -2074,7 +2979,10 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2074
2979
|
if (newStart) settings.startCommand = newStart;
|
|
2075
2980
|
const newPort = await prompt(`Port: ${chalk.dim(`(${settings.port})`)}`);
|
|
2076
2981
|
if (newPort) settings.port = parseInt(newPort, 10) || settings.port;
|
|
2982
|
+
const newRoot = await prompt(`Root directory: ${chalk.dim(`(${settings.rootDir})`)}`);
|
|
2983
|
+
if (newRoot) settings.rootDir = newRoot;
|
|
2077
2984
|
}
|
|
2985
|
+
printSummary(settings);
|
|
2078
2986
|
const spinService = spinner("Criando serviço...");
|
|
2079
2987
|
const service = await withRetry(() => client.services.create({
|
|
2080
2988
|
projectId,
|
|
@@ -2088,51 +2996,104 @@ async function createServiceFlow(client, projectId, projectName, repoName) {
|
|
|
2088
2996
|
}));
|
|
2089
2997
|
spinService.stop();
|
|
2090
2998
|
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2091
|
-
await promptEnvVars(
|
|
2999
|
+
if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key));
|
|
3000
|
+
const serviceKey = settings.rootDir !== "." ? settings.rootDir : settings.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
2092
3001
|
saveConfig({
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
port: settings.port
|
|
3002
|
+
...existingConfig,
|
|
3003
|
+
services: {
|
|
3004
|
+
...existingConfig.services,
|
|
3005
|
+
[serviceKey]: {
|
|
3006
|
+
id: service.id,
|
|
3007
|
+
name: service.name,
|
|
3008
|
+
type: settings.type.toLowerCase(),
|
|
3009
|
+
root: settings.rootDir,
|
|
3010
|
+
branch,
|
|
3011
|
+
build: settings.buildCommand || settings.outputDir ? {
|
|
3012
|
+
command: settings.buildCommand ?? void 0,
|
|
3013
|
+
outputDir: settings.outputDir ?? void 0
|
|
3014
|
+
} : void 0,
|
|
3015
|
+
runtime: {
|
|
3016
|
+
command: settings.startCommand ?? void 0,
|
|
3017
|
+
port: settings.port
|
|
3018
|
+
}
|
|
2111
3019
|
}
|
|
2112
|
-
}
|
|
2113
|
-
|
|
3020
|
+
},
|
|
3021
|
+
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2114
3022
|
});
|
|
2115
|
-
info(`Arquivo ${getConfigFileName()}
|
|
2116
|
-
|
|
3023
|
+
info(`Arquivo ${getConfigFileName()} atualizado.`);
|
|
3024
|
+
await triggerDeploy(service.id, service.name);
|
|
3025
|
+
}
|
|
3026
|
+
function isMultipleOrgsError(error) {
|
|
3027
|
+
if (!(error instanceof Error)) return false;
|
|
3028
|
+
const e = error;
|
|
3029
|
+
return e.data?.code === "MULTIPLE_ORGS" && Array.isArray(e.data?.organizations);
|
|
3030
|
+
}
|
|
3031
|
+
async function resolveOrgIfNeeded(nonInteractive) {
|
|
3032
|
+
if ((await requireAuth({ nonInteractive })).organizationId) return;
|
|
3033
|
+
if (loadConfig()?.project?.id) return;
|
|
3034
|
+
const client = await getClient();
|
|
3035
|
+
try {
|
|
3036
|
+
await client.projects.list();
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
if (!isMultipleOrgsError(error)) throw error;
|
|
3039
|
+
const orgs = error.data.organizations;
|
|
3040
|
+
info(`Você tem acesso a ${orgs.length} organizações.`);
|
|
3041
|
+
saveConfig$1({ organizationId: await promptSelect("Selecione a organização para este deploy:", orgs.map((org) => ({
|
|
3042
|
+
label: `${org.name} ${chalk.dim(`(${org.slug})`)}`,
|
|
3043
|
+
value: org.id
|
|
3044
|
+
}))) });
|
|
3045
|
+
success("Organização salva.");
|
|
3046
|
+
}
|
|
2117
3047
|
}
|
|
2118
|
-
const deployCommand = new Command("deploy").description("Fazer deploy do serviço (auto-detecta projeto pelo git)").option("-a, --all", "Deploy todos os serviços do monorepo").option("--service <service>", "Deploy de um serviço específico (chave ou nome)").option("-v, --verbose", "Mostrar logs detalhados do servidor").action(async (options) => {
|
|
3048
|
+
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("--new", "Criar e adicionar um novo serviço ao projeto existente").option("-y, --yes", "Auto-confirmar tudo (modo não-interativo)").option("-v, --verbose", "Mostrar logs detalhados do servidor").action(async (options) => {
|
|
2119
3049
|
if (options.verbose) process.env.VELOZ_VERBOSE = "true";
|
|
2120
3050
|
try {
|
|
2121
|
-
|
|
3051
|
+
printBanner("Deploy");
|
|
3052
|
+
await autoUpdate();
|
|
3053
|
+
await requireAuth({ nonInteractive: options.yes });
|
|
3054
|
+
await resolveOrgIfNeeded(options.yes);
|
|
3055
|
+
const activeEnv = getActiveEnv();
|
|
3056
|
+
if (activeEnv) {
|
|
3057
|
+
const rawConfig = loadRawConfig();
|
|
3058
|
+
if (rawConfig && !rawConfig.environments?.[activeEnv]) {
|
|
3059
|
+
info(`Ambiente "${activeEnv}" não encontrado. Configurando...`);
|
|
3060
|
+
await createEnvironmentFlow(rawConfig, activeEnv, { yes: options.yes });
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
if (options.new) {
|
|
3064
|
+
const existingConfig = loadConfig();
|
|
3065
|
+
if (!existingConfig) info("Nenhum veloz.json encontrado. Criando projeto do zero...");
|
|
3066
|
+
else {
|
|
3067
|
+
await addServiceFlow(existingConfig, {
|
|
3068
|
+
yes: options.yes,
|
|
3069
|
+
app: options.app
|
|
3070
|
+
});
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
2122
3074
|
const configuredServices = await findServicesFromConfig();
|
|
2123
3075
|
if (configuredServices.length > 0) {
|
|
2124
3076
|
if (options.service) {
|
|
2125
3077
|
const found = configuredServices.find((s) => s.key === options.service || s.serviceName.toLowerCase() === options.service.toLowerCase() || s.serviceId === options.service);
|
|
2126
3078
|
if (!found) {
|
|
2127
|
-
|
|
2128
|
-
|
|
3079
|
+
output({
|
|
3080
|
+
type: "error",
|
|
3081
|
+
message: `Serviço '${options.service}' não encontrado.`,
|
|
3082
|
+
available: configuredServices.map((s) => ({
|
|
3083
|
+
key: s.key,
|
|
3084
|
+
name: s.serviceName
|
|
3085
|
+
}))
|
|
3086
|
+
}, () => {
|
|
3087
|
+
const available = configuredServices.map((s) => ` • ${s.key} (${s.serviceName})`).join("\n");
|
|
3088
|
+
console.error(chalk.red(`\n✗ Serviço '${options.service}' não encontrado.\n\nServiços disponíveis:\n${available}`));
|
|
3089
|
+
});
|
|
2129
3090
|
process.exit(1);
|
|
2130
3091
|
}
|
|
2131
3092
|
await triggerDeploy(found.serviceId, found.serviceName);
|
|
2132
3093
|
return;
|
|
2133
3094
|
}
|
|
2134
|
-
if (options.all || configuredServices.length === 1) {
|
|
2135
|
-
if (configuredServices.length > 1) {
|
|
3095
|
+
if (options.all || options.yes || configuredServices.length === 1) {
|
|
3096
|
+
if (configuredServices.length > 1 && !options.yes) {
|
|
2136
3097
|
console.log(chalk.cyan(`\n🚀 Fazendo deploy de ${configuredServices.length} serviço(s):\n`));
|
|
2137
3098
|
for (const service of configuredServices) {
|
|
2138
3099
|
const relPath = relative(process.cwd(), service.path) || ".";
|
|
@@ -2145,10 +3106,7 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2145
3106
|
}
|
|
2146
3107
|
}
|
|
2147
3108
|
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
|
-
}
|
|
3109
|
+
else await deployServicesInParallel(await computeExtraFilesForServices(configuredServices));
|
|
2152
3110
|
return;
|
|
2153
3111
|
} else {
|
|
2154
3112
|
console.log(chalk.bold("\n📦 Serviços disponíveis:\n"));
|
|
@@ -2165,24 +3123,21 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2165
3123
|
return;
|
|
2166
3124
|
}
|
|
2167
3125
|
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
|
-
}
|
|
3126
|
+
else await deployServicesInParallel(await computeExtraFilesForServices(selectedServices));
|
|
2172
3127
|
return;
|
|
2173
3128
|
}
|
|
2174
3129
|
}
|
|
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
|
-
}
|
|
3130
|
+
if (!isGitRepo()) handleError(/* @__PURE__ */ new Error("Este diretório não é um repositório git. Inicialize com `git init` e adicione um remote."));
|
|
2179
3131
|
info("Detectando repositório git...");
|
|
2180
3132
|
const remote = getGitRemote();
|
|
2181
|
-
if (!remote)
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
3133
|
+
if (!remote) handleError(/* @__PURE__ */ new Error("Nenhum remote 'origin' encontrado. Adicione um remote git."));
|
|
3134
|
+
output({
|
|
3135
|
+
type: "git_repo",
|
|
3136
|
+
owner: remote.owner,
|
|
3137
|
+
repo: remote.repo
|
|
3138
|
+
}, () => {
|
|
3139
|
+
console.log(chalk.white(` ${chalk.bold("Repositório:")} ${remote.owner}/${remote.repo}`));
|
|
3140
|
+
});
|
|
2186
3141
|
const client = await getClient();
|
|
2187
3142
|
const spin = spinner("Buscando projeto...");
|
|
2188
3143
|
const project = await withRetry(() => client.projects.findByRepo({
|
|
@@ -2198,6 +3153,10 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2198
3153
|
const svc = project.services[0];
|
|
2199
3154
|
serviceId = svc.id;
|
|
2200
3155
|
serviceName = svc.name;
|
|
3156
|
+
} else if (options.yes) {
|
|
3157
|
+
const svc = project.services[0];
|
|
3158
|
+
serviceId = svc.id;
|
|
3159
|
+
serviceName = svc.name;
|
|
2201
3160
|
} else {
|
|
2202
3161
|
serviceId = await promptSelect("Selecione o serviço:", project.services.map((s) => ({
|
|
2203
3162
|
label: `${s.name} (${s.type} — branch: ${s.branch})`,
|
|
@@ -2206,6 +3165,8 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2206
3165
|
serviceName = project.services.find((s) => s.id === serviceId)?.name ?? "";
|
|
2207
3166
|
}
|
|
2208
3167
|
const selectedService = project.services.find((s) => s.id === serviceId);
|
|
3168
|
+
const detection = detectLocalRepo();
|
|
3169
|
+
const detectedType = selectedService?.type?.toLowerCase() ?? "web";
|
|
2209
3170
|
saveConfig({
|
|
2210
3171
|
version: "1.0",
|
|
2211
3172
|
project: {
|
|
@@ -2215,24 +3176,27 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2215
3176
|
services: { main: {
|
|
2216
3177
|
id: serviceId,
|
|
2217
3178
|
name: serviceName,
|
|
2218
|
-
type:
|
|
3179
|
+
type: detectedType,
|
|
2219
3180
|
root: ".",
|
|
2220
3181
|
branch: selectedService?.branch
|
|
2221
3182
|
} },
|
|
2222
3183
|
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2223
3184
|
});
|
|
2224
3185
|
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2225
|
-
await triggerDeploy(serviceId);
|
|
3186
|
+
await triggerDeploy(serviceId, void 0, detection);
|
|
2226
3187
|
return;
|
|
2227
3188
|
}
|
|
2228
3189
|
if (project && project.services.length === 0) {
|
|
2229
3190
|
info(`Projeto encontrado: ${chalk.bold(project.name)}`);
|
|
2230
3191
|
info("Nenhum serviço configurado. Vamos criar um.");
|
|
2231
|
-
await triggerDeploy(await createServiceFlow(
|
|
3192
|
+
await triggerDeploy(await createServiceFlow(project.id, project.name, remote.repo, {
|
|
3193
|
+
yes: options.yes,
|
|
3194
|
+
app: options.app
|
|
3195
|
+
}));
|
|
2232
3196
|
return;
|
|
2233
3197
|
}
|
|
2234
3198
|
info("Projeto não encontrado. Vamos criar um novo.");
|
|
2235
|
-
const projectName = await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
|
|
3199
|
+
const projectName = options.yes ? remote.repo : await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
|
|
2236
3200
|
const spinProject = spinner("Criando projeto...");
|
|
2237
3201
|
const newProject = await withRetry(() => client.projects.create({
|
|
2238
3202
|
name: projectName,
|
|
@@ -2241,7 +3205,10 @@ const deployCommand = new Command("deploy").description("Fazer deploy do serviç
|
|
|
2241
3205
|
}));
|
|
2242
3206
|
spinProject.stop();
|
|
2243
3207
|
success(`Projeto criado: ${chalk.bold(newProject.name)}`);
|
|
2244
|
-
await triggerDeploy(await createServiceFlow(
|
|
3208
|
+
await triggerDeploy(await createServiceFlow(newProject.id, newProject.name, remote.repo, {
|
|
3209
|
+
yes: options.yes,
|
|
3210
|
+
app: options.app
|
|
3211
|
+
}));
|
|
2245
3212
|
} catch (error) {
|
|
2246
3213
|
handleError(error);
|
|
2247
3214
|
}
|
|
@@ -2283,10 +3250,14 @@ const TAG_COLORS = [
|
|
|
2283
3250
|
chalk.red
|
|
2284
3251
|
];
|
|
2285
3252
|
function getServiceTag(name, maxLen, colorIndex) {
|
|
3253
|
+
const mode = getOutputMode();
|
|
3254
|
+
if (mode === "json" || mode === "github-actions" || mode === "plain") return `[${name.padEnd(maxLen)}]`;
|
|
2286
3255
|
const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
|
|
2287
3256
|
return color(`[${name.padEnd(maxLen)}]`);
|
|
2288
3257
|
}
|
|
2289
3258
|
function getServiceHeader(name, colorIndex) {
|
|
3259
|
+
const mode = getOutputMode();
|
|
3260
|
+
if (mode === "json" || mode === "github-actions" || mode === "plain") return `── ${name} ──`;
|
|
2290
3261
|
const color = TAG_COLORS[colorIndex % TAG_COLORS.length];
|
|
2291
3262
|
return color(`── ${name} ──`);
|
|
2292
3263
|
}
|
|
@@ -2385,7 +3356,8 @@ function resolveAllServices(serviceFlag) {
|
|
|
2385
3356
|
function formatTime(timestamp) {
|
|
2386
3357
|
return chalk.dim(new Date(timestamp).toLocaleTimeString("pt-BR"));
|
|
2387
3358
|
}
|
|
2388
|
-
async function streamFollow(
|
|
3359
|
+
async function streamFollow(services, maxNameLen, tailLines) {
|
|
3360
|
+
const client = await getClient();
|
|
2389
3361
|
const showTags = services.length > 1;
|
|
2390
3362
|
const streams = services.map(async ({ service, index }) => {
|
|
2391
3363
|
const tag = showTags ? `${getServiceTag(service.name, maxNameLen, index)} ` : "";
|
|
@@ -2394,14 +3366,22 @@ async function streamFollow(client, services, maxNameLen, tailLines) {
|
|
|
2394
3366
|
serviceId: service.id,
|
|
2395
3367
|
tailLines: Math.ceil(tailLines / services.length)
|
|
2396
3368
|
});
|
|
2397
|
-
for await (const entry of stream)
|
|
3369
|
+
for await (const entry of stream) output({
|
|
3370
|
+
type: "log_entry",
|
|
3371
|
+
service: service.name,
|
|
3372
|
+
timestamp: entry.timestamp,
|
|
3373
|
+
message: entry.message
|
|
3374
|
+
}, () => {
|
|
3375
|
+
console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
|
|
3376
|
+
});
|
|
2398
3377
|
} catch {
|
|
2399
3378
|
console.log(`${tag}${chalk.red("Erro ao conectar ao stream de logs")}`);
|
|
2400
3379
|
}
|
|
2401
3380
|
});
|
|
2402
3381
|
await Promise.allSettled(streams);
|
|
2403
3382
|
}
|
|
2404
|
-
async function fetchRecent(
|
|
3383
|
+
async function fetchRecent(services, maxNameLen, tailLines) {
|
|
3384
|
+
const client = await getClient();
|
|
2405
3385
|
const showTags = services.length > 1;
|
|
2406
3386
|
const allEntries = (await Promise.allSettled(services.map(async ({ service, index }) => {
|
|
2407
3387
|
return (await client.logs.getRecent({
|
|
@@ -2417,28 +3397,36 @@ async function fetchRecent(client, services, maxNameLen, tailLines) {
|
|
|
2417
3397
|
info("Nenhum log encontrado.");
|
|
2418
3398
|
return;
|
|
2419
3399
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
3400
|
+
output({
|
|
3401
|
+
type: "logs",
|
|
3402
|
+
entries: allEntries.map((e) => ({
|
|
3403
|
+
service: e.serviceName,
|
|
3404
|
+
timestamp: e.timestamp,
|
|
3405
|
+
message: e.message
|
|
3406
|
+
}))
|
|
3407
|
+
}, () => {
|
|
3408
|
+
console.log();
|
|
3409
|
+
for (const entry of allEntries) {
|
|
3410
|
+
const tag = showTags ? `${getServiceTag(entry.serviceName, maxNameLen, entry.serviceIndex)} ` : "";
|
|
3411
|
+
console.log(`${tag}${formatTime(entry.timestamp)} ${entry.message}`);
|
|
3412
|
+
}
|
|
3413
|
+
console.log();
|
|
3414
|
+
info(`Mostrando ${allEntries.length} linha(s). Use ${chalk.bold("--follow")} para acompanhar em tempo real.`);
|
|
3415
|
+
});
|
|
2427
3416
|
}
|
|
2428
3417
|
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
3418
|
const spin = spinner("Carregando logs...");
|
|
2430
3419
|
try {
|
|
2431
3420
|
const { services, maxNameLen } = resolveAllServices(opts.service);
|
|
2432
|
-
const client = await getClient();
|
|
2433
3421
|
const tailLines = parseInt(opts.tail, 10) || 50;
|
|
2434
3422
|
if (opts.follow) {
|
|
2435
3423
|
spin.text = services.length > 1 ? `Conectando a ${services.length} serviço(s)...` : "Conectando ao streaming de logs...";
|
|
2436
3424
|
spin.stop();
|
|
2437
3425
|
info("Streaming de logs ativo. Pressione Ctrl+C para sair.\n");
|
|
2438
|
-
await streamFollow(
|
|
3426
|
+
await streamFollow(services, maxNameLen, tailLines);
|
|
2439
3427
|
} else {
|
|
2440
3428
|
spin.stop();
|
|
2441
|
-
await fetchRecent(
|
|
3429
|
+
await fetchRecent(services, maxNameLen, tailLines);
|
|
2442
3430
|
}
|
|
2443
3431
|
} catch (error) {
|
|
2444
3432
|
spin.stop();
|
|
@@ -2472,7 +3460,11 @@ envCommand.command("list").alias("listar").description("Listar variáveis de amb
|
|
|
2472
3460
|
chalk.bold(v.key),
|
|
2473
3461
|
chalk.dim(v.maskedValue),
|
|
2474
3462
|
new Date(v.updatedAt).toLocaleDateString("pt-BR")
|
|
2475
|
-
]))
|
|
3463
|
+
]), envVars.map((v) => ({
|
|
3464
|
+
key: v.key,
|
|
3465
|
+
maskedValue: v.maskedValue,
|
|
3466
|
+
updatedAt: v.updatedAt
|
|
3467
|
+
})));
|
|
2476
3468
|
}
|
|
2477
3469
|
spin.stop();
|
|
2478
3470
|
if (totalVars === 0 && !showHeaders) info("Nenhuma variável de ambiente configurada.");
|
|
@@ -2490,15 +3482,13 @@ envCommand.command("set <pares...>").description("Definir variável de ambiente
|
|
|
2490
3482
|
const eqIndex = par.indexOf("=");
|
|
2491
3483
|
if (eqIndex === -1) {
|
|
2492
3484
|
spin.stop();
|
|
2493
|
-
|
|
2494
|
-
process.exit(1);
|
|
3485
|
+
handleError(/* @__PURE__ */ new Error(`Formato inválido: "${par}". Use CHAVE=VALOR.`));
|
|
2495
3486
|
}
|
|
2496
3487
|
const key = par.slice(0, eqIndex);
|
|
2497
3488
|
const value = par.slice(eqIndex + 1);
|
|
2498
3489
|
if (!key) {
|
|
2499
3490
|
spin.stop();
|
|
2500
|
-
|
|
2501
|
-
process.exit(1);
|
|
3491
|
+
handleError(/* @__PURE__ */ new Error("Chave não pode estar vazia."));
|
|
2502
3492
|
}
|
|
2503
3493
|
await client.envVars.set({
|
|
2504
3494
|
serviceId,
|
|
@@ -2540,38 +3530,32 @@ envCommand.command("import [arquivo]").description("Importar variáveis de ambie
|
|
|
2540
3530
|
let envContent = "";
|
|
2541
3531
|
if (arquivo) {
|
|
2542
3532
|
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
|
-
}
|
|
3533
|
+
if (!existsSync(filePath)) handleError(/* @__PURE__ */ new Error(`Arquivo não encontrado: ${arquivo}`));
|
|
2547
3534
|
envContent = readFileSync(filePath, "utf-8");
|
|
2548
3535
|
info(`Importando de ${chalk.bold(arquivo)}...`);
|
|
2549
3536
|
} 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
3537
|
const rl = readline.createInterface({
|
|
2553
3538
|
input: process.stdin,
|
|
2554
|
-
output: process.stdout
|
|
3539
|
+
output: isInteractive() ? process.stdout : void 0
|
|
2555
3540
|
});
|
|
2556
|
-
|
|
3541
|
+
if (isInteractive()) {
|
|
3542
|
+
console.log(chalk.cyan("\n📋 Modo de colagem interativo"));
|
|
3543
|
+
console.log(chalk.dim("Cole seu conteúdo .env abaixo. Pressione Ctrl+D (ou Ctrl+Z no Windows) quando terminar:\n"));
|
|
3544
|
+
}
|
|
3545
|
+
const lines = [];
|
|
2557
3546
|
await new Promise((resolve$1) => {
|
|
2558
|
-
rl.on("line", (line) =>
|
|
2559
|
-
|
|
2560
|
-
});
|
|
2561
|
-
rl.on("close", () => {
|
|
2562
|
-
resolve$1();
|
|
2563
|
-
});
|
|
3547
|
+
rl.on("line", (line) => lines.push(line));
|
|
3548
|
+
rl.on("close", () => resolve$1());
|
|
2564
3549
|
});
|
|
2565
|
-
envContent = lines
|
|
3550
|
+
envContent = lines.join("\n");
|
|
2566
3551
|
}
|
|
2567
3552
|
const envVars = {};
|
|
2568
|
-
const
|
|
2569
|
-
for (const line of lines) {
|
|
3553
|
+
for (const line of envContent.split("\n")) {
|
|
2570
3554
|
const trimmed = line.trim();
|
|
2571
3555
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2572
3556
|
const eqIndex = trimmed.indexOf("=");
|
|
2573
3557
|
if (eqIndex === -1) continue;
|
|
2574
|
-
|
|
3558
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
2575
3559
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
2576
3560
|
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
2577
3561
|
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\");
|
|
@@ -2579,7 +3563,7 @@ envCommand.command("import [arquivo]").description("Importar variáveis de ambie
|
|
|
2579
3563
|
}
|
|
2580
3564
|
const varsCount = Object.keys(envVars).length;
|
|
2581
3565
|
if (varsCount === 0) {
|
|
2582
|
-
|
|
3566
|
+
warn("Nenhuma variável válida encontrada.");
|
|
2583
3567
|
return;
|
|
2584
3568
|
}
|
|
2585
3569
|
console.log(chalk.bold("\n📝 Variáveis a serem importadas:\n"));
|
|
@@ -2628,20 +3612,31 @@ envCommand.command("export [arquivo]").description("Exportar variáveis de ambie
|
|
|
2628
3612
|
return;
|
|
2629
3613
|
}
|
|
2630
3614
|
const envContent = envVars.map((v) => `${v.key}=${v.maskedValue}`).join("\n");
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
3615
|
+
output({
|
|
3616
|
+
type: "env_export",
|
|
3617
|
+
vars: envVars.map((v) => ({
|
|
3618
|
+
key: v.key,
|
|
3619
|
+
maskedValue: v.maskedValue
|
|
3620
|
+
}))
|
|
3621
|
+
}, () => {
|
|
3622
|
+
if (arquivo) {
|
|
3623
|
+
writeFileSync(resolve(process.cwd(), arquivo), envContent + "\n", "utf-8");
|
|
3624
|
+
success(`Variáveis exportadas para ${chalk.bold(arquivo)}`);
|
|
3625
|
+
console.log(chalk.dim("Nota: Valores estão mascarados por segurança."));
|
|
3626
|
+
} else {
|
|
3627
|
+
console.log(chalk.bold("\n# Variáveis de Ambiente (valores mascarados)\n"));
|
|
3628
|
+
console.log(envContent);
|
|
3629
|
+
console.log();
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
2640
3632
|
} catch (error) {
|
|
2641
3633
|
spin.stop();
|
|
2642
3634
|
handleError(error);
|
|
2643
3635
|
}
|
|
2644
3636
|
});
|
|
3637
|
+
function warn(message) {
|
|
3638
|
+
console.log(chalk.yellow(`⚠ ${message}`));
|
|
3639
|
+
}
|
|
2645
3640
|
|
|
2646
3641
|
//#endregion
|
|
2647
3642
|
//#region src/commands/domains.ts
|
|
@@ -2674,12 +3669,18 @@ domainsCommand.command("list").alias("listar").description("Listar domínios dos
|
|
|
2674
3669
|
"TLS",
|
|
2675
3670
|
"Tipo"
|
|
2676
3671
|
], domains.map((d) => [
|
|
2677
|
-
d.id
|
|
3672
|
+
d.id,
|
|
2678
3673
|
chalk.bold(d.domain),
|
|
2679
3674
|
d.verified ? chalk.green("✓ Sim") : chalk.yellow("✗ Não"),
|
|
2680
3675
|
tlsStatusLabels[d.tlsStatus] ?? d.tlsStatus,
|
|
2681
3676
|
d.isAutoGenerated ? "Auto" : "Personalizado"
|
|
2682
|
-
]))
|
|
3677
|
+
]), domains.map((d) => ({
|
|
3678
|
+
id: d.id,
|
|
3679
|
+
domain: d.domain,
|
|
3680
|
+
verified: d.verified,
|
|
3681
|
+
tlsStatus: d.tlsStatus,
|
|
3682
|
+
isAutoGenerated: d.isAutoGenerated
|
|
3683
|
+
})));
|
|
2683
3684
|
}
|
|
2684
3685
|
spin.stop();
|
|
2685
3686
|
if (totalDomains === 0 && !showHeaders) info("Nenhum domínio configurado.");
|
|
@@ -2697,12 +3698,19 @@ domainsCommand.command("add <dominio>").alias("adicionar").description("Adiciona
|
|
|
2697
3698
|
domain: dominio
|
|
2698
3699
|
});
|
|
2699
3700
|
spin.stop();
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
3701
|
+
output({
|
|
3702
|
+
type: "domain_added",
|
|
3703
|
+
id: result.id,
|
|
3704
|
+
domain: dominio,
|
|
3705
|
+
dnsInstruction: result.dnsInstruction
|
|
3706
|
+
}, () => {
|
|
3707
|
+
success(`Domínio ${chalk.bold(dominio)} adicionado com sucesso!`);
|
|
3708
|
+
console.log();
|
|
3709
|
+
console.log(chalk.yellow.bold(" Instruções de DNS:"));
|
|
3710
|
+
console.log(chalk.white(` ${result.dnsInstruction}`));
|
|
3711
|
+
console.log();
|
|
3712
|
+
info(`Após configurar o DNS, execute: ${chalk.bold(`veloz domains verify ${result.id}`)}`);
|
|
3713
|
+
});
|
|
2706
3714
|
} catch (error) {
|
|
2707
3715
|
spin.stop();
|
|
2708
3716
|
handleError(error);
|
|
@@ -2713,13 +3721,19 @@ domainsCommand.command("verify <domainId>").alias("verificar").description("Veri
|
|
|
2713
3721
|
try {
|
|
2714
3722
|
const result = await (await getClient()).domains.verify({ domainId });
|
|
2715
3723
|
spin.stop();
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3724
|
+
output({
|
|
3725
|
+
type: "domain_verified",
|
|
3726
|
+
domain: result.domain.domain,
|
|
3727
|
+
verified: result.verified
|
|
3728
|
+
}, () => {
|
|
3729
|
+
if (result.verified) {
|
|
3730
|
+
success(`Domínio ${chalk.bold(result.domain.domain)} verificado com sucesso!`);
|
|
3731
|
+
info("O certificado TLS será provisionado automaticamente.");
|
|
3732
|
+
} else {
|
|
3733
|
+
console.log(chalk.yellow(`\n⚠ Domínio ${chalk.bold(result.domain.domain)} ainda não verificado.`));
|
|
3734
|
+
info("Verifique se o CNAME foi propagado e tente novamente.");
|
|
3735
|
+
}
|
|
3736
|
+
});
|
|
2723
3737
|
} catch (error) {
|
|
2724
3738
|
spin.stop();
|
|
2725
3739
|
handleError(error);
|
|
@@ -2762,14 +3776,22 @@ configCommand.command("show").description("Mostrar configurações atuais dos se
|
|
|
2762
3776
|
const client = await getClient();
|
|
2763
3777
|
const showHeaders = services.length > 1;
|
|
2764
3778
|
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
|
-
}
|
|
3779
|
+
const configs = [];
|
|
3780
|
+
for (const { service: svcConfig } of services) configs.push(await client.services.get({ serviceId: svcConfig.id }));
|
|
2772
3781
|
spin.stop();
|
|
3782
|
+
output({
|
|
3783
|
+
type: "service_configs",
|
|
3784
|
+
services: configs
|
|
3785
|
+
}, () => {
|
|
3786
|
+
for (let i = 0; i < services.length; i++) {
|
|
3787
|
+
const { service: svcConfig, index } = services[i];
|
|
3788
|
+
const service = configs[i];
|
|
3789
|
+
if (showHeaders) console.log(`\n${getServiceHeader(svcConfig.name, index)}\n`);
|
|
3790
|
+
else console.log(chalk.bold("\n📋 Configurações do Serviço\n"));
|
|
3791
|
+
printServiceConfig(service);
|
|
3792
|
+
console.log();
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
2773
3795
|
} catch (error) {
|
|
2774
3796
|
handleError(error);
|
|
2775
3797
|
}
|
|
@@ -2800,13 +3822,18 @@ configCommand.command("set").description("Atualizar configurações do serviço"
|
|
|
2800
3822
|
});
|
|
2801
3823
|
spin.stop();
|
|
2802
3824
|
success("Configurações atualizadas com sucesso!");
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
3825
|
+
output({
|
|
3826
|
+
type: "config_updated",
|
|
3827
|
+
updates
|
|
3828
|
+
}, () => {
|
|
3829
|
+
console.log(chalk.dim("\nValores atualizados:"));
|
|
3830
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
3831
|
+
const displayKey = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
|
|
3832
|
+
console.log(` ${chalk.bold(displayKey)}: ${formatValue(value)}`);
|
|
3833
|
+
}
|
|
3834
|
+
console.log();
|
|
3835
|
+
info("Execute 'veloz deploy' para aplicar as mudanças.");
|
|
3836
|
+
});
|
|
2810
3837
|
} catch (error) {
|
|
2811
3838
|
handleError(error);
|
|
2812
3839
|
}
|
|
@@ -2825,8 +3852,8 @@ configCommand.command("edit").description("Editar configurações interativament
|
|
|
2825
3852
|
if (name) updates.name = name;
|
|
2826
3853
|
const buildCmd = await prompt(`Build command ${chalk.dim(`(${service.buildCommand || "—"})`)}: `);
|
|
2827
3854
|
if (buildCmd) updates.buildCommand = buildCmd === "none" ? null : buildCmd;
|
|
2828
|
-
const startCmd
|
|
2829
|
-
if (startCmd
|
|
3855
|
+
const startCmd = await prompt(`Start command ${chalk.dim(`(${service.startCommand || "—"})`)}: `);
|
|
3856
|
+
if (startCmd) updates.startCommand = startCmd === "none" ? null : startCmd;
|
|
2830
3857
|
const port = await prompt(`Porta ${chalk.dim(`(${service.port})`)}: `);
|
|
2831
3858
|
if (port) updates.port = parseInt(port, 10);
|
|
2832
3859
|
const rootDir = await prompt(`Diretório raiz ${chalk.dim(`(${service.rootDirectory || "/"})`)}: `);
|
|
@@ -2888,15 +3915,9 @@ configCommand.command("reset").description("Resetar configurações para os padr
|
|
|
2888
3915
|
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
3916
|
try {
|
|
2890
3917
|
const config = loadConfig();
|
|
2891
|
-
if (!config)
|
|
2892
|
-
console.error(chalk.red("\n✗ Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
|
|
2893
|
-
process.exit(1);
|
|
2894
|
-
}
|
|
3918
|
+
if (!config) handleError(/* @__PURE__ */ new Error("Nenhum projeto configurado. Execute 'veloz deploy' primeiro."));
|
|
2895
3919
|
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
|
-
}
|
|
3920
|
+
if (services.length === 0) handleError(/* @__PURE__ */ new Error("Nenhum serviço encontrado na configuração."));
|
|
2900
3921
|
if (services.length === 1) {
|
|
2901
3922
|
info("Apenas um serviço configurado — já é o padrão.");
|
|
2902
3923
|
return;
|
|
@@ -2906,13 +3927,8 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
|
|
|
2906
3927
|
if (servicoArg) {
|
|
2907
3928
|
const found = services.find(([key, service]) => key === servicoArg || service.name.toLowerCase() === servicoArg.toLowerCase());
|
|
2908
3929
|
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);
|
|
3930
|
+
const available = services.map(([key, svc]) => ` • ${key} (${svc.name})`).join("\n");
|
|
3931
|
+
handleError(/* @__PURE__ */ new Error(`Serviço '${servicoArg}' não encontrado.\n\nServiços disponíveis:\n${available}`));
|
|
2916
3932
|
}
|
|
2917
3933
|
selectedKey = found[0];
|
|
2918
3934
|
} else selectedKey = await promptSelect("Qual serviço usar como padrão?", services.map(([key, service]) => ({
|
|
@@ -2921,9 +3937,138 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
|
|
|
2921
3937
|
})));
|
|
2922
3938
|
setDefaultServiceKey(selectedKey);
|
|
2923
3939
|
const selectedService = config.services[selectedKey];
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3940
|
+
output({
|
|
3941
|
+
type: "default_service",
|
|
3942
|
+
key: selectedKey,
|
|
3943
|
+
name: selectedService.name
|
|
3944
|
+
}, () => {
|
|
3945
|
+
success(`Serviço padrão: ${chalk.bold(selectedService.name)} (${selectedKey})`);
|
|
3946
|
+
console.log(chalk.dim(`\nComandos como ${chalk.white("logs")}, ${chalk.white("env")}, ${chalk.white("config")} vão usar este serviço automaticamente.`));
|
|
3947
|
+
console.log(chalk.dim(`Use ${chalk.white("--service <nome>")} para sobrescrever pontualmente.`));
|
|
3948
|
+
});
|
|
3949
|
+
} catch (error) {
|
|
3950
|
+
handleError(error);
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
//#endregion
|
|
3955
|
+
//#region src/commands/apikey.ts
|
|
3956
|
+
async function authFetch(path, options = {}) {
|
|
3957
|
+
const config = await requireAuth();
|
|
3958
|
+
const url = `${config.apiUrl}/api/auth${path}`;
|
|
3959
|
+
const res = await fetch(url, {
|
|
3960
|
+
...options,
|
|
3961
|
+
headers: {
|
|
3962
|
+
"Content-Type": "application/json",
|
|
3963
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
3964
|
+
...options.headers
|
|
3965
|
+
}
|
|
3966
|
+
});
|
|
3967
|
+
if (!res.ok) {
|
|
3968
|
+
const body = await res.text().catch(() => "");
|
|
3969
|
+
throw new Error(`Erro ${res.status}: ${body || res.statusText}`);
|
|
3970
|
+
}
|
|
3971
|
+
return res.json();
|
|
3972
|
+
}
|
|
3973
|
+
const apikeyCommand = new Command("apikey").description("Gerenciar chaves de API");
|
|
3974
|
+
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) => {
|
|
3975
|
+
try {
|
|
3976
|
+
const s = spinner("Criando chave de API...");
|
|
3977
|
+
const body = {
|
|
3978
|
+
name: opts.name,
|
|
3979
|
+
prefix: "veloz"
|
|
3980
|
+
};
|
|
3981
|
+
if (opts.expire === false) body.expiresIn = null;
|
|
3982
|
+
const data = await authFetch("/api-key/create", {
|
|
3983
|
+
method: "POST",
|
|
3984
|
+
body: JSON.stringify(body)
|
|
3985
|
+
});
|
|
3986
|
+
s.stop();
|
|
3987
|
+
output({
|
|
3988
|
+
type: "apikey_created",
|
|
3989
|
+
key: data.key,
|
|
3990
|
+
name: data.name,
|
|
3991
|
+
expiresAt: data.expiresAt
|
|
3992
|
+
}, () => {
|
|
3993
|
+
success("Chave de API criada!");
|
|
3994
|
+
console.log();
|
|
3995
|
+
console.log(chalk.bold(" Chave:"), chalk.green(data.key));
|
|
3996
|
+
console.log(chalk.bold(" Nome:"), data.name);
|
|
3997
|
+
if (data.expiresAt) console.log(chalk.bold(" Expira:"), new Date(data.expiresAt).toLocaleDateString("pt-BR"));
|
|
3998
|
+
else console.log(chalk.bold(" Expira:"), "Nunca");
|
|
3999
|
+
console.log();
|
|
4000
|
+
console.log(chalk.yellow(" Guarde esta chave — ela não será exibida novamente."));
|
|
4001
|
+
console.log();
|
|
4002
|
+
console.log(chalk.dim(" Uso em CI:"));
|
|
4003
|
+
console.log(chalk.dim(` VELOZ_API_KEY=${data.key} veloz deploy -y --service web`));
|
|
4004
|
+
console.log();
|
|
4005
|
+
console.log(chalk.dim(" GitHub Actions:"));
|
|
4006
|
+
console.log(chalk.dim(` gh secret set VELOZ_API_KEY --body "${data.key}"`));
|
|
4007
|
+
console.log();
|
|
4008
|
+
});
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
handleError(error);
|
|
4011
|
+
}
|
|
4012
|
+
});
|
|
4013
|
+
apikeyCommand.command("list").alias("listar").description("Listar chaves de API").action(async () => {
|
|
4014
|
+
try {
|
|
4015
|
+
const s = spinner("Buscando chaves...");
|
|
4016
|
+
const data = await authFetch("/api-key/list", { method: "GET" });
|
|
4017
|
+
s.stop();
|
|
4018
|
+
const keys = Array.isArray(data) ? data : data.apiKeys ?? [];
|
|
4019
|
+
if (!keys || keys.length === 0) {
|
|
4020
|
+
info("Nenhuma chave de API encontrada.");
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
printTable([
|
|
4024
|
+
"Nome",
|
|
4025
|
+
"ID",
|
|
4026
|
+
"Criado",
|
|
4027
|
+
"Expira"
|
|
4028
|
+
], keys.map((k) => [
|
|
4029
|
+
k.name ?? "-",
|
|
4030
|
+
k.id,
|
|
4031
|
+
new Date(k.createdAt).toLocaleDateString("pt-BR"),
|
|
4032
|
+
k.expiresAt ? new Date(k.expiresAt).toLocaleDateString("pt-BR") : "Nunca"
|
|
4033
|
+
]), keys.map((k) => ({
|
|
4034
|
+
name: k.name,
|
|
4035
|
+
id: k.id,
|
|
4036
|
+
createdAt: k.createdAt,
|
|
4037
|
+
expiresAt: k.expiresAt
|
|
4038
|
+
})));
|
|
4039
|
+
} catch (error) {
|
|
4040
|
+
handleError(error);
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
apikeyCommand.command("delete <keyId>").alias("deletar").description("Deletar uma chave de API").action(async (keyId) => {
|
|
4044
|
+
try {
|
|
4045
|
+
const s = spinner("Deletando chave...");
|
|
4046
|
+
await authFetch("/api-key/delete", {
|
|
4047
|
+
method: "POST",
|
|
4048
|
+
body: JSON.stringify({ keyId })
|
|
4049
|
+
});
|
|
4050
|
+
s.stop();
|
|
4051
|
+
success("Chave deletada.");
|
|
4052
|
+
} catch (error) {
|
|
4053
|
+
handleError(error);
|
|
4054
|
+
}
|
|
4055
|
+
});
|
|
4056
|
+
|
|
4057
|
+
//#endregion
|
|
4058
|
+
//#region src/commands/whoami.ts
|
|
4059
|
+
const whoamiCommand = new Command("whoami").description("Mostrar usuário autenticado").action(async () => {
|
|
4060
|
+
try {
|
|
4061
|
+
const user = await (await getClient()).me();
|
|
4062
|
+
output({
|
|
4063
|
+
type: "user",
|
|
4064
|
+
name: user.name,
|
|
4065
|
+
email: user.email
|
|
4066
|
+
}, () => {
|
|
4067
|
+
console.log();
|
|
4068
|
+
console.log(` ${chalk.bold("Nome:")} ${user.name}`);
|
|
4069
|
+
console.log(` ${chalk.bold("Email:")} ${user.email}`);
|
|
4070
|
+
console.log();
|
|
4071
|
+
});
|
|
2927
4072
|
} catch (error) {
|
|
2928
4073
|
handleError(error);
|
|
2929
4074
|
}
|
|
@@ -2931,8 +4076,23 @@ const useCommand = new Command("use").description("Selecionar qual serviço usar
|
|
|
2931
4076
|
|
|
2932
4077
|
//#endregion
|
|
2933
4078
|
//#region src/index.ts
|
|
2934
|
-
const
|
|
2935
|
-
const
|
|
4079
|
+
const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.11").option("--output <format>", "Formato de saída: fancy, json, github-actions, plain").option("--env <environment>", "Ambiente alvo (ex: preview, staging)").hook("preAction", (thisCommand) => {
|
|
4080
|
+
const opts = thisCommand.opts();
|
|
4081
|
+
if (opts.output) {
|
|
4082
|
+
const valid = [
|
|
4083
|
+
"fancy",
|
|
4084
|
+
"json",
|
|
4085
|
+
"github-actions",
|
|
4086
|
+
"plain"
|
|
4087
|
+
];
|
|
4088
|
+
if (!valid.includes(opts.output)) {
|
|
4089
|
+
console.error(`Formato de saída inválido: "${opts.output}". Use: ${valid.join(", ")}`);
|
|
4090
|
+
process.exit(1);
|
|
4091
|
+
}
|
|
4092
|
+
setOutputMode(opts.output);
|
|
4093
|
+
}
|
|
4094
|
+
if (opts.env) setActiveEnv(opts.env);
|
|
4095
|
+
});
|
|
2936
4096
|
program.addCommand(loginCommand);
|
|
2937
4097
|
program.addCommand(logoutCommand);
|
|
2938
4098
|
program.addCommand(projectsCommand);
|
|
@@ -2943,6 +4103,8 @@ program.addCommand(envCommand);
|
|
|
2943
4103
|
program.addCommand(domainsCommand);
|
|
2944
4104
|
program.addCommand(configCommand);
|
|
2945
4105
|
program.addCommand(useCommand);
|
|
4106
|
+
program.addCommand(apikeyCommand);
|
|
4107
|
+
program.addCommand(whoamiCommand);
|
|
2946
4108
|
program.parse();
|
|
2947
4109
|
|
|
2948
4110
|
//#endregion
|