onveloz 0.0.0-beta.13 → 0.0.0-beta.14
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 +324 -48
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -72,6 +72,7 @@ const SERVICE_TYPES = [
|
|
|
72
72
|
];
|
|
73
73
|
const BUILD_TIMEOUT_MS = 600 * 1e3;
|
|
74
74
|
const DEPLOY_TIMEOUT_MS = 300 * 1e3;
|
|
75
|
+
const DEFAULT_SLEEP_CHECK_INTERVAL_MS = 300 * 1e3;
|
|
75
76
|
|
|
76
77
|
//#endregion
|
|
77
78
|
//#region ../../packages/config/veloz-config.ts
|
|
@@ -118,6 +119,17 @@ const EnvVarDefinitionSchema = z.object({
|
|
|
118
119
|
required: z.boolean().default(false).optional(),
|
|
119
120
|
example: z.string().optional()
|
|
120
121
|
});
|
|
122
|
+
const VolumeConfigSchema = z.object({
|
|
123
|
+
name: z.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/),
|
|
124
|
+
mountPath: z.string().min(1).regex(/^\//).refine((value) => value !== "/", "Montar na raiz '/' não é permitido").refine((value) => value !== "/tmp" && !value.startsWith("/tmp/"), "Caminhos dentro de /tmp não são permitidos").refine((value) => ![
|
|
125
|
+
"/proc",
|
|
126
|
+
"/sys",
|
|
127
|
+
"/dev",
|
|
128
|
+
"/etc",
|
|
129
|
+
"/var/run"
|
|
130
|
+
].some((p) => value === p || value.startsWith(p + "/")), "Caminho de montagem não permitido por segurança"),
|
|
131
|
+
sizeGb: z.number().int().min(1).max(100).optional().default(1)
|
|
132
|
+
});
|
|
121
133
|
const ServiceConfigSchema = z.object({
|
|
122
134
|
id: z.string(),
|
|
123
135
|
name: z.string(),
|
|
@@ -127,6 +139,7 @@ const ServiceConfigSchema = z.object({
|
|
|
127
139
|
build: BuildConfigSchema.optional(),
|
|
128
140
|
runtime: RuntimeConfigSchema.optional(),
|
|
129
141
|
env: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), EnvVarDefinitionSchema).optional(),
|
|
142
|
+
volumes: z.array(VolumeConfigSchema).optional(),
|
|
130
143
|
resources: ResourcesSchema.optional()
|
|
131
144
|
});
|
|
132
145
|
const ProjectConfigSchema = z.object({
|
|
@@ -886,6 +899,8 @@ const linkCommand = new Command("link").description("Verificar vínculo do proje
|
|
|
886
899
|
|
|
887
900
|
//#endregion
|
|
888
901
|
//#region ../../packages/api/src/lib/framework-detector.ts
|
|
902
|
+
const SOURCE_FILE_REGEX = /\.(?:[cm]?[jt]sx?)$/;
|
|
903
|
+
const NODE_FS_IMPORT_REGEX = /(?:from\s+["'](?:node:)?fs(?:\/promises)?["']|require\(\s*["'](?:node:)?fs(?:\/promises)?["']\s*\)|import\(\s*["'](?:node:)?fs(?:\/promises)?["']\s*\))/;
|
|
889
904
|
function safeParsePkg(content) {
|
|
890
905
|
try {
|
|
891
906
|
return JSON.parse(content);
|
|
@@ -897,6 +912,12 @@ function pmRun(pm, script) {
|
|
|
897
912
|
if (pm === "yarn") return `yarn ${script}`;
|
|
898
913
|
return `${pm} run ${script}`;
|
|
899
914
|
}
|
|
915
|
+
function isSourceFile(path) {
|
|
916
|
+
return SOURCE_FILE_REGEX.test(path);
|
|
917
|
+
}
|
|
918
|
+
function usesNodeFs(content) {
|
|
919
|
+
return NODE_FS_IMPORT_REGEX.test(content);
|
|
920
|
+
}
|
|
900
921
|
function detectFramework(pkgJsonStr, pm) {
|
|
901
922
|
const pkg = safeParsePkg(pkgJsonStr);
|
|
902
923
|
if (!pkg) return null;
|
|
@@ -1070,6 +1091,7 @@ function extractEnvVars(files) {
|
|
|
1070
1091
|
function analyzeRepo(files) {
|
|
1071
1092
|
const packageManager = detectPackageManager$1(files);
|
|
1072
1093
|
const envVars = extractEnvVars(files);
|
|
1094
|
+
const sourceEntries = Object.entries(files).filter(([path]) => isSourceFile(path));
|
|
1073
1095
|
const rootPkgContent = files["package.json"];
|
|
1074
1096
|
const rootPkg = rootPkgContent ? safeParsePkg(rootPkgContent) : null;
|
|
1075
1097
|
const isMonorepo = "pnpm-workspace.yaml" in files || !!rootPkg?.workspaces;
|
|
@@ -1099,7 +1121,8 @@ function analyzeRepo(files) {
|
|
|
1099
1121
|
monorepoApps.push({
|
|
1100
1122
|
name: appName,
|
|
1101
1123
|
path: appPath,
|
|
1102
|
-
framework: appFramework
|
|
1124
|
+
framework: appFramework,
|
|
1125
|
+
usesNodeFs: sourceEntries.some(([filePath$1, fileContent]) => filePath$1.startsWith(`${appPath}/`) && usesNodeFs(fileContent))
|
|
1103
1126
|
});
|
|
1104
1127
|
}
|
|
1105
1128
|
return {
|
|
@@ -1107,7 +1130,8 @@ function analyzeRepo(files) {
|
|
|
1107
1130
|
framework,
|
|
1108
1131
|
envVars,
|
|
1109
1132
|
isMonorepo,
|
|
1110
|
-
monorepoApps
|
|
1133
|
+
monorepoApps,
|
|
1134
|
+
usesNodeFs: sourceEntries.some(([, content]) => usesNodeFs(content))
|
|
1111
1135
|
};
|
|
1112
1136
|
}
|
|
1113
1137
|
|
|
@@ -1406,7 +1430,10 @@ async function deployServicesInParallel(services) {
|
|
|
1406
1430
|
const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
|
|
1407
1431
|
const deploymentPromises = services.map(async (service) => {
|
|
1408
1432
|
try {
|
|
1409
|
-
const deployment = await withRetry(() => client.deployments.create({
|
|
1433
|
+
const deployment = await withRetry(() => client.deployments.create({
|
|
1434
|
+
serviceId: service.serviceId,
|
|
1435
|
+
serviceConfig: service.serviceConfig
|
|
1436
|
+
}));
|
|
1410
1437
|
await withRetry(() => uploadSource(deployment.id, projectRoot, service.extraFiles));
|
|
1411
1438
|
trackDeployment(deployment.id);
|
|
1412
1439
|
progressMap.set(service.serviceId, {
|
|
@@ -1781,7 +1808,7 @@ const LOGO_LINES = [
|
|
|
1781
1808
|
];
|
|
1782
1809
|
const BRAND_COLOR = "#FF4D00";
|
|
1783
1810
|
function getVersion() {
|
|
1784
|
-
return "0.0.0-beta.
|
|
1811
|
+
return "0.0.0-beta.14";
|
|
1785
1812
|
}
|
|
1786
1813
|
function printBanner(subtitle) {
|
|
1787
1814
|
const mode = getOutputMode();
|
|
@@ -1815,7 +1842,8 @@ function resolveServiceConf(velozConfig, serviceId) {
|
|
|
1815
1842
|
for (const [, conf] of Object.entries(velozConfig.services)) if (conf.id === serviceId) {
|
|
1816
1843
|
const merged = mergeServiceWithDefaults(conf, velozConfig.defaults);
|
|
1817
1844
|
return {
|
|
1818
|
-
type: merged.type,
|
|
1845
|
+
type: merged.type?.toUpperCase(),
|
|
1846
|
+
branch: merged.branch,
|
|
1819
1847
|
buildCommand: merged.build?.command ?? void 0,
|
|
1820
1848
|
startCommand: merged.runtime?.command ?? void 0,
|
|
1821
1849
|
port: merged.runtime?.port ?? void 0,
|
|
@@ -1829,29 +1857,11 @@ function resolveServiceConf(velozConfig, serviceId) {
|
|
|
1829
1857
|
nodeVersion: merged.build?.nodeVersion ?? void 0,
|
|
1830
1858
|
nixpkgsArchive: merged.build?.nixpkgsArchive ?? void 0,
|
|
1831
1859
|
packageManager: merged.build?.packageManager ?? void 0,
|
|
1832
|
-
installCommand: merged.build?.installCommand ?? void 0
|
|
1860
|
+
installCommand: merged.build?.installCommand ?? void 0,
|
|
1861
|
+
volumes: merged.volumes ?? void 0
|
|
1833
1862
|
};
|
|
1834
1863
|
}
|
|
1835
1864
|
}
|
|
1836
|
-
async function syncServiceConfig(client, serviceId, conf) {
|
|
1837
|
-
await withRetry(() => client.services.update({
|
|
1838
|
-
serviceId,
|
|
1839
|
-
type: conf.type?.toUpperCase(),
|
|
1840
|
-
port: conf.port,
|
|
1841
|
-
instanceCount: conf.instanceCount,
|
|
1842
|
-
cpuLimit: conf.cpuLimit,
|
|
1843
|
-
memoryLimit: conf.memoryLimit,
|
|
1844
|
-
buildCommand: conf.buildCommand,
|
|
1845
|
-
startCommand: conf.startCommand,
|
|
1846
|
-
rootDirectory: conf.rootDirectory,
|
|
1847
|
-
healthCheckPath: conf.healthCheckPath,
|
|
1848
|
-
aptPackages: conf.aptPackages,
|
|
1849
|
-
nodeVersion: conf.nodeVersion,
|
|
1850
|
-
nixpkgsArchive: conf.nixpkgsArchive,
|
|
1851
|
-
packageManager: conf.packageManager,
|
|
1852
|
-
installCommand: conf.installCommand
|
|
1853
|
-
}));
|
|
1854
|
-
}
|
|
1855
1865
|
|
|
1856
1866
|
//#endregion
|
|
1857
1867
|
//#region src/lib/deploy-checks.ts
|
|
@@ -2285,7 +2295,7 @@ async function fetchLatestVersion() {
|
|
|
2285
2295
|
async function autoUpdate() {
|
|
2286
2296
|
const pm = detectPackageManager();
|
|
2287
2297
|
if (!pm) return;
|
|
2288
|
-
const currentVersion = "0.0.0-beta.
|
|
2298
|
+
const currentVersion = "0.0.0-beta.14";
|
|
2289
2299
|
const latestVersion = await fetchLatestVersion();
|
|
2290
2300
|
if (!latestVersion || latestVersion === currentVersion) return;
|
|
2291
2301
|
const installCmd = getInstallCommand(pm, latestVersion);
|
|
@@ -2324,7 +2334,6 @@ function prepareExtraFiles(_detection, serviceConfig) {
|
|
|
2324
2334
|
}
|
|
2325
2335
|
async function computeExtraFilesForServices(services) {
|
|
2326
2336
|
const velozConfig = loadConfig();
|
|
2327
|
-
const client = await getClient();
|
|
2328
2337
|
const results = [];
|
|
2329
2338
|
const allWarnings = [];
|
|
2330
2339
|
for (const svc of services) {
|
|
@@ -2347,10 +2356,11 @@ async function computeExtraFilesForServices(services) {
|
|
|
2347
2356
|
});
|
|
2348
2357
|
for (const svc of services) {
|
|
2349
2358
|
const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
|
|
2350
|
-
if (serviceConf) await syncServiceConfig(client, svc.serviceId, serviceConf);
|
|
2351
2359
|
const detection = detectLocalRepo(serviceConf?.rootDirectory || ".");
|
|
2360
|
+
warnIfEphemeralFsDetected(detection, serviceConf, svc.serviceName);
|
|
2352
2361
|
results.push({
|
|
2353
2362
|
...svc,
|
|
2363
|
+
serviceConfig: serviceConf,
|
|
2354
2364
|
extraFiles: prepareExtraFiles(detection, serviceConf)
|
|
2355
2365
|
});
|
|
2356
2366
|
}
|
|
@@ -2364,8 +2374,9 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
|
|
|
2364
2374
|
if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
|
|
2365
2375
|
const client = await getClient();
|
|
2366
2376
|
const serviceConf = resolveServiceConf(loadConfig(), serviceId);
|
|
2367
|
-
|
|
2368
|
-
|
|
2377
|
+
const detection = preDetection ?? detectLocalRepo();
|
|
2378
|
+
warnIfEphemeralFsDetected(detection, serviceConf, serviceName ?? void 0);
|
|
2379
|
+
const extraFiles = prepareExtraFiles(detection, serviceConf);
|
|
2369
2380
|
const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
|
|
2370
2381
|
if (warnings.length > 0) {
|
|
2371
2382
|
spinUpload.stop();
|
|
@@ -2373,7 +2384,10 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
|
|
|
2373
2384
|
spinUpload.start();
|
|
2374
2385
|
}
|
|
2375
2386
|
spinUpload.text = "Iniciando deploy...";
|
|
2376
|
-
const deployment = await withRetry(() => client.deployments.create({
|
|
2387
|
+
const deployment = await withRetry(() => client.deployments.create({
|
|
2388
|
+
serviceId,
|
|
2389
|
+
serviceConfig: serviceConf
|
|
2390
|
+
}));
|
|
2377
2391
|
spinUpload.text = "Fazendo upload do código...";
|
|
2378
2392
|
await withRetry(() => uploadSource(deployment.id, process.cwd(), extraFiles));
|
|
2379
2393
|
spinUpload.stop();
|
|
@@ -2390,6 +2404,25 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
|
|
|
2390
2404
|
handleError(error);
|
|
2391
2405
|
}
|
|
2392
2406
|
}
|
|
2407
|
+
function warnIfEphemeralFsDetected(detection, serviceConf, serviceLabel) {
|
|
2408
|
+
if (!detection.usesNodeFs || (serviceConf?.volumes?.length ?? 0) > 0) return;
|
|
2409
|
+
warn$1(`Uso de fs/node:fs detectado${serviceLabel ? ` no serviço ${chalk.bold(serviceLabel)}` : ""}. O filesystem do container é efêmero; configure um volume em veloz.json ou use 'veloz volumes create'.`);
|
|
2410
|
+
}
|
|
2411
|
+
async function maybeConfigurePersistentVolume(serviceConfig, detection, opts, serviceLabel) {
|
|
2412
|
+
if (!detection.usesNodeFs || (serviceConfig.volumes?.length ?? 0) > 0) return;
|
|
2413
|
+
if (opts.yes) return;
|
|
2414
|
+
if (!await promptConfirm(`Deseja adicionar um volume persistente para ${serviceLabel}?`, true)) return;
|
|
2415
|
+
const name = await prompt(`Nome do volume ${chalk.dim("(data)")}:`) || "data";
|
|
2416
|
+
const mountPath = await prompt(`Mount path ${chalk.dim("(/data)")}:`) || "/data";
|
|
2417
|
+
const sizeInput = await prompt(`Tamanho em GB ${chalk.dim("(1)")}:`);
|
|
2418
|
+
const parsedSize = Number.parseInt(sizeInput || "1", 10);
|
|
2419
|
+
serviceConfig.volumes = [{
|
|
2420
|
+
name,
|
|
2421
|
+
mountPath,
|
|
2422
|
+
sizeGb: Number.isInteger(parsedSize) && parsedSize > 0 ? parsedSize : 1
|
|
2423
|
+
}];
|
|
2424
|
+
info(`Volume persistente configurado para ${serviceLabel}.`);
|
|
2425
|
+
}
|
|
2393
2426
|
async function findServicesFromConfig() {
|
|
2394
2427
|
const config = loadConfig();
|
|
2395
2428
|
if (!config) return [];
|
|
@@ -2413,6 +2446,41 @@ function readLocalFile(path) {
|
|
|
2413
2446
|
return null;
|
|
2414
2447
|
}
|
|
2415
2448
|
}
|
|
2449
|
+
const SOURCE_FILE_NAME = /\.(?:[cm]?[jt]sx?)$/;
|
|
2450
|
+
const SOURCE_SCAN_IGNORED_DIRS = new Set([
|
|
2451
|
+
".git",
|
|
2452
|
+
".next",
|
|
2453
|
+
".turbo",
|
|
2454
|
+
"coverage",
|
|
2455
|
+
"dist",
|
|
2456
|
+
"build",
|
|
2457
|
+
"node_modules"
|
|
2458
|
+
]);
|
|
2459
|
+
const MAX_SOURCE_FILES = 200;
|
|
2460
|
+
const MAX_SOURCE_FILE_BYTES = 64 * 1024;
|
|
2461
|
+
function collectSourceFiles(scanRoot, outputPrefix, files, counter) {
|
|
2462
|
+
if (!existsSync(scanRoot) || counter.count >= MAX_SOURCE_FILES) return;
|
|
2463
|
+
try {
|
|
2464
|
+
const entries = readdirSync(scanRoot, { withFileTypes: true });
|
|
2465
|
+
for (const entry of entries) {
|
|
2466
|
+
if (counter.count >= MAX_SOURCE_FILES) return;
|
|
2467
|
+
const fullPath = join(scanRoot, entry.name);
|
|
2468
|
+
const relativePath = outputPrefix ? `${outputPrefix}/${entry.name}` : entry.name;
|
|
2469
|
+
if (entry.isDirectory()) {
|
|
2470
|
+
if (SOURCE_SCAN_IGNORED_DIRS.has(entry.name)) continue;
|
|
2471
|
+
collectSourceFiles(fullPath, relativePath, files, counter);
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
if (!entry.isFile() || !SOURCE_FILE_NAME.test(entry.name)) continue;
|
|
2475
|
+
try {
|
|
2476
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
2477
|
+
if (Buffer.byteLength(content, "utf-8") > MAX_SOURCE_FILE_BYTES) continue;
|
|
2478
|
+
files[relativePath] = content;
|
|
2479
|
+
counter.count += 1;
|
|
2480
|
+
} catch {}
|
|
2481
|
+
}
|
|
2482
|
+
} catch {}
|
|
2483
|
+
}
|
|
2416
2484
|
function printSummary(settings) {
|
|
2417
2485
|
const { type: serviceType, ...rest } = settings;
|
|
2418
2486
|
output({
|
|
@@ -2437,6 +2505,7 @@ function printSummary(settings) {
|
|
|
2437
2505
|
}
|
|
2438
2506
|
function detectLocalRepo(basePath = ".") {
|
|
2439
2507
|
const files = {};
|
|
2508
|
+
const sourceCounter = { count: 0 };
|
|
2440
2509
|
const pkgJson = readLocalFile(join(basePath, "package.json"));
|
|
2441
2510
|
if (pkgJson) files["package.json"] = pkgJson;
|
|
2442
2511
|
const envExample = readLocalFile(join(basePath, ".env.example"));
|
|
@@ -2482,22 +2551,26 @@ function detectLocalRepo(basePath = ".") {
|
|
|
2482
2551
|
}
|
|
2483
2552
|
}
|
|
2484
2553
|
}
|
|
2554
|
+
if (workspacePatterns.length === 0) collectSourceFiles(resolve(process.cwd(), basePath), "", files, sourceCounter);
|
|
2485
2555
|
for (const pattern of workspacePatterns) {
|
|
2486
2556
|
const hasGlob = /\/?\*\*?$/.test(pattern);
|
|
2487
2557
|
const base = pattern.replace(/\/?\*\*?$/, "");
|
|
2488
2558
|
if (hasGlob) {
|
|
2489
|
-
const dirPath = resolve(process.cwd(), base);
|
|
2559
|
+
const dirPath = resolve(process.cwd(), basePath, base);
|
|
2490
2560
|
if (!existsSync(dirPath)) continue;
|
|
2491
2561
|
try {
|
|
2492
2562
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
2493
2563
|
for (const entry of entries) if (entry.isDirectory()) {
|
|
2494
|
-
const
|
|
2495
|
-
|
|
2564
|
+
const appPrefix = `${base}/${entry.name}`;
|
|
2565
|
+
const nestedPkg = readLocalFile(join(basePath, appPrefix, "package.json"));
|
|
2566
|
+
if (nestedPkg) files[`${appPrefix}/package.json`] = nestedPkg;
|
|
2567
|
+
collectSourceFiles(resolve(process.cwd(), basePath, appPrefix), appPrefix, files, sourceCounter);
|
|
2496
2568
|
}
|
|
2497
2569
|
} catch {}
|
|
2498
2570
|
} else {
|
|
2499
2571
|
const nestedPkg = readLocalFile(join(basePath, base, "package.json"));
|
|
2500
2572
|
if (nestedPkg) files[`${base}/package.json`] = nestedPkg;
|
|
2573
|
+
collectSourceFiles(resolve(process.cwd(), basePath, base), base, files, sourceCounter);
|
|
2501
2574
|
}
|
|
2502
2575
|
}
|
|
2503
2576
|
return analyzeRepo(files);
|
|
@@ -2540,7 +2613,8 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2540
2613
|
framework: a.framework?.name ?? null,
|
|
2541
2614
|
buildCommand: a.framework?.buildCommand ?? null,
|
|
2542
2615
|
startCommand: a.framework?.startCommand ?? null,
|
|
2543
|
-
port: a.framework?.port ?? 3e3
|
|
2616
|
+
port: a.framework?.port ?? 3e3,
|
|
2617
|
+
usesNodeFs: a.usesNodeFs
|
|
2544
2618
|
}));
|
|
2545
2619
|
let selectedApps;
|
|
2546
2620
|
if (opts.yes) if (opts.app) {
|
|
@@ -2590,7 +2664,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2590
2664
|
if (newPort) app.port = parseInt(newPort, 10) || app.port;
|
|
2591
2665
|
}
|
|
2592
2666
|
}
|
|
2593
|
-
const config = {
|
|
2667
|
+
const config$1 = {
|
|
2594
2668
|
version: "1.0",
|
|
2595
2669
|
project: {
|
|
2596
2670
|
id: projectId,
|
|
@@ -2619,7 +2693,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2619
2693
|
service: service$1,
|
|
2620
2694
|
app
|
|
2621
2695
|
});
|
|
2622
|
-
config.services[app.root] = {
|
|
2696
|
+
config$1.services[app.root] = {
|
|
2623
2697
|
id: service$1.id,
|
|
2624
2698
|
name: service$1.name,
|
|
2625
2699
|
type: "web",
|
|
@@ -2631,8 +2705,9 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2631
2705
|
port: app.port ?? 3e3
|
|
2632
2706
|
}
|
|
2633
2707
|
};
|
|
2708
|
+
await maybeConfigurePersistentVolume(config$1.services[app.root], detectLocalRepo(app.root), opts, app.name);
|
|
2634
2709
|
}
|
|
2635
|
-
saveConfig(config);
|
|
2710
|
+
saveConfig(config$1);
|
|
2636
2711
|
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2637
2712
|
if (!opts.yes) for (const { service: service$1, app } of createdServices) {
|
|
2638
2713
|
console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
|
|
@@ -2643,6 +2718,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2643
2718
|
serviceId: service$1.id,
|
|
2644
2719
|
serviceName: app.name,
|
|
2645
2720
|
path: resolve(process.cwd(), app.root),
|
|
2721
|
+
serviceConfig: resolveServiceConf(config$1, service$1.id),
|
|
2646
2722
|
extraFiles: prepareExtraFiles(detectLocalRepo(app.root), {
|
|
2647
2723
|
type: "WEB",
|
|
2648
2724
|
buildCommand: app.buildCommand ?? void 0,
|
|
@@ -2708,7 +2784,7 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2708
2784
|
spinService.stop();
|
|
2709
2785
|
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2710
2786
|
if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key));
|
|
2711
|
-
|
|
2787
|
+
const config = {
|
|
2712
2788
|
version: "1.0",
|
|
2713
2789
|
project: {
|
|
2714
2790
|
id: projectId,
|
|
@@ -2730,7 +2806,9 @@ async function createServiceFlow(projectId, projectName, repoName, opts = {}) {
|
|
|
2730
2806
|
}
|
|
2731
2807
|
} },
|
|
2732
2808
|
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2733
|
-
}
|
|
2809
|
+
};
|
|
2810
|
+
await maybeConfigurePersistentVolume(config.services.main, detection, opts, service.name);
|
|
2811
|
+
saveConfig(config);
|
|
2734
2812
|
info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
|
|
2735
2813
|
return service.id;
|
|
2736
2814
|
}
|
|
@@ -2819,7 +2897,8 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
2819
2897
|
framework: a.framework?.name ?? null,
|
|
2820
2898
|
buildCommand: a.framework?.buildCommand ?? null,
|
|
2821
2899
|
startCommand: a.framework?.startCommand ?? null,
|
|
2822
|
-
port: a.framework?.port ?? 3e3
|
|
2900
|
+
port: a.framework?.port ?? 3e3,
|
|
2901
|
+
usesNodeFs: a.usesNodeFs
|
|
2823
2902
|
})).filter((a) => !existingRoots.has(a.root));
|
|
2824
2903
|
if (availableApps.length === 0) {
|
|
2825
2904
|
info("Todos os apps do monorepo já estão configurados.");
|
|
@@ -2877,7 +2956,7 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
2877
2956
|
if (newPort) app.port = parseInt(newPort, 10) || app.port;
|
|
2878
2957
|
}
|
|
2879
2958
|
}
|
|
2880
|
-
const updatedConfig = {
|
|
2959
|
+
const updatedConfig$1 = {
|
|
2881
2960
|
...existingConfig,
|
|
2882
2961
|
services: { ...existingConfig.services }
|
|
2883
2962
|
};
|
|
@@ -2900,7 +2979,7 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
2900
2979
|
service: service$1,
|
|
2901
2980
|
app
|
|
2902
2981
|
});
|
|
2903
|
-
updatedConfig.services[app.root] = {
|
|
2982
|
+
updatedConfig$1.services[app.root] = {
|
|
2904
2983
|
id: service$1.id,
|
|
2905
2984
|
name: service$1.name,
|
|
2906
2985
|
type: "web",
|
|
@@ -2912,9 +2991,10 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
2912
2991
|
port: app.port ?? 3e3
|
|
2913
2992
|
}
|
|
2914
2993
|
};
|
|
2994
|
+
await maybeConfigurePersistentVolume(updatedConfig$1.services[app.root], detectLocalRepo(app.root), opts, app.name);
|
|
2915
2995
|
}
|
|
2916
|
-
updatedConfig.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2917
|
-
saveConfig(updatedConfig);
|
|
2996
|
+
updatedConfig$1.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2997
|
+
saveConfig(updatedConfig$1);
|
|
2918
2998
|
info(`Arquivo ${getConfigFileName()} atualizado com ${createdServices.length} novo(s) serviço(s).`);
|
|
2919
2999
|
if (!opts.yes) for (const { service: service$1, app } of createdServices) {
|
|
2920
3000
|
console.log(chalk.cyan(`\n── Configurando variáveis: ${app.name} ──\n`));
|
|
@@ -2925,6 +3005,7 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
2925
3005
|
serviceId: service$1.id,
|
|
2926
3006
|
serviceName: app.name,
|
|
2927
3007
|
path: resolve(process.cwd(), app.root),
|
|
3008
|
+
serviceConfig: resolveServiceConf(updatedConfig$1, service$1.id),
|
|
2928
3009
|
extraFiles: prepareExtraFiles(detectLocalRepo(app.root), {
|
|
2929
3010
|
type: "WEB",
|
|
2930
3011
|
buildCommand: app.buildCommand ?? void 0,
|
|
@@ -2984,7 +3065,7 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
2984
3065
|
success(`Serviço criado: ${chalk.bold(service.name)}`);
|
|
2985
3066
|
if (!opts.yes) await promptEnvVars(service.id, detection.envVars.map((v) => v.key));
|
|
2986
3067
|
const serviceKey = settings.rootDir !== "." ? settings.rootDir : settings.name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
2987
|
-
|
|
3068
|
+
const updatedConfig = {
|
|
2988
3069
|
...existingConfig,
|
|
2989
3070
|
services: {
|
|
2990
3071
|
...existingConfig.services,
|
|
@@ -3005,7 +3086,9 @@ async function addServiceFlow(existingConfig, opts) {
|
|
|
3005
3086
|
}
|
|
3006
3087
|
},
|
|
3007
3088
|
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
3008
|
-
}
|
|
3089
|
+
};
|
|
3090
|
+
await maybeConfigurePersistentVolume(updatedConfig.services[serviceKey], detection, opts, service.name);
|
|
3091
|
+
saveConfig(updatedConfig);
|
|
3009
3092
|
info(`Arquivo ${getConfigFileName()} atualizado.`);
|
|
3010
3093
|
await triggerDeploy(service.id, service.name);
|
|
3011
3094
|
}
|
|
@@ -3732,6 +3815,198 @@ domainsCommand.command("delete <domainId>").alias("deletar").description("Remove
|
|
|
3732
3815
|
}
|
|
3733
3816
|
});
|
|
3734
3817
|
|
|
3818
|
+
//#endregion
|
|
3819
|
+
//#region src/lib/volume-config.ts
|
|
3820
|
+
function updateServiceVolumesInConfig(serviceKey, updater) {
|
|
3821
|
+
const config = loadRawConfig();
|
|
3822
|
+
if (!config) return false;
|
|
3823
|
+
const currentService = config.services[serviceKey];
|
|
3824
|
+
if (!currentService) return false;
|
|
3825
|
+
currentService.volumes = updater([...currentService.volumes ?? []]);
|
|
3826
|
+
config.updated = (/* @__PURE__ */ new Date()).toISOString();
|
|
3827
|
+
saveConfig(config);
|
|
3828
|
+
return true;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
//#endregion
|
|
3832
|
+
//#region src/commands/volumes.ts
|
|
3833
|
+
const STATUS_COLORS = {
|
|
3834
|
+
READY: chalk.green,
|
|
3835
|
+
PENDING: chalk.yellow,
|
|
3836
|
+
ERROR: chalk.red
|
|
3837
|
+
};
|
|
3838
|
+
function formatStatus(status) {
|
|
3839
|
+
return (STATUS_COLORS[status] ?? chalk.dim)(status);
|
|
3840
|
+
}
|
|
3841
|
+
async function resolveRemoteVolume(serviceId, volumeRef) {
|
|
3842
|
+
const volume = (await (await getClient()).volumes.list({ serviceId })).find((entry) => entry.id === volumeRef || entry.name === volumeRef);
|
|
3843
|
+
if (!volume) throw new Error(`Volume '${volumeRef}' não encontrado para este serviço.`);
|
|
3844
|
+
return volume;
|
|
3845
|
+
}
|
|
3846
|
+
async function promptAndSyncDeployment(serviceId) {
|
|
3847
|
+
if (!await promptConfirm("Reiniciar o serviço agora para aplicar as alterações de volumes?", false)) {
|
|
3848
|
+
info("O serviço não foi reiniciado. Reinicie manualmente quando desejar.");
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
const spin = spinner("Reiniciando serviço...");
|
|
3852
|
+
try {
|
|
3853
|
+
await (await getClient()).volumes.syncDeployment({ serviceId });
|
|
3854
|
+
spin.stop();
|
|
3855
|
+
success("Serviço reiniciando com os volumes atualizados.");
|
|
3856
|
+
} catch (error) {
|
|
3857
|
+
spin.stop();
|
|
3858
|
+
handleError(error);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
const volumesCommand = new Command("volumes").alias("volume").description("Gerenciar volumes persistentes");
|
|
3862
|
+
volumesCommand.command("list").alias("listar").description("Listar volumes dos serviços").option("--service <service>", "Filtrar por serviço (chave ou nome)").action(async (opts) => {
|
|
3863
|
+
const spin = spinner("Carregando volumes...");
|
|
3864
|
+
try {
|
|
3865
|
+
const { services } = resolveAllServices(opts.service);
|
|
3866
|
+
const client = await getClient();
|
|
3867
|
+
const showHeaders = services.length > 1;
|
|
3868
|
+
let totalVolumes = 0;
|
|
3869
|
+
const allVolumes = [];
|
|
3870
|
+
for (const { service, index } of services) {
|
|
3871
|
+
const volumes = await client.volumes.list({ serviceId: service.id });
|
|
3872
|
+
totalVolumes += volumes.length;
|
|
3873
|
+
allVolumes.push({
|
|
3874
|
+
service,
|
|
3875
|
+
index,
|
|
3876
|
+
volumes
|
|
3877
|
+
});
|
|
3878
|
+
}
|
|
3879
|
+
spin.stop();
|
|
3880
|
+
for (const { service, index, volumes } of allVolumes) {
|
|
3881
|
+
if (showHeaders) console.log(`\n${getServiceHeader(service.name, index)}`);
|
|
3882
|
+
if (volumes.length === 0) {
|
|
3883
|
+
info("Nenhum volume configurado.");
|
|
3884
|
+
continue;
|
|
3885
|
+
}
|
|
3886
|
+
printTable([
|
|
3887
|
+
"Nome",
|
|
3888
|
+
"Montagem",
|
|
3889
|
+
"Tamanho",
|
|
3890
|
+
"Status"
|
|
3891
|
+
], volumes.map((volume) => [
|
|
3892
|
+
chalk.bold(volume.name),
|
|
3893
|
+
chalk.dim(volume.mountPath),
|
|
3894
|
+
`${volume.sizeGb} GB`,
|
|
3895
|
+
formatStatus(volume.status)
|
|
3896
|
+
]), volumes.map((volume) => ({
|
|
3897
|
+
id: volume.id,
|
|
3898
|
+
name: volume.name,
|
|
3899
|
+
mountPath: volume.mountPath,
|
|
3900
|
+
sizeGb: volume.sizeGb,
|
|
3901
|
+
status: volume.status
|
|
3902
|
+
})));
|
|
3903
|
+
}
|
|
3904
|
+
if (totalVolumes === 0 && !showHeaders) info("Nenhum volume configurado.");
|
|
3905
|
+
} catch (error) {
|
|
3906
|
+
spin.stop();
|
|
3907
|
+
handleError(error);
|
|
3908
|
+
}
|
|
3909
|
+
});
|
|
3910
|
+
volumesCommand.command("create <name>").alias("criar").description("Criar um volume persistente").requiredOption("--mount <path>", "Caminho de montagem no container").option("--size <gb>", "Tamanho em GB", "1").option("--service <service>", "Serviço alvo (chave ou nome)").option("--restart", "Reiniciar o serviço imediatamente após criar").action(async (name, opts) => {
|
|
3911
|
+
let spin;
|
|
3912
|
+
try {
|
|
3913
|
+
const { key, service } = await resolveService(opts.service);
|
|
3914
|
+
const client = await getClient();
|
|
3915
|
+
const sizeGb = Number.parseInt(opts.size, 10);
|
|
3916
|
+
if (!Number.isInteger(sizeGb) || sizeGb < 1 || sizeGb > 100) throw new Error("O tamanho do volume deve ser um inteiro entre 1 e 100 GB.");
|
|
3917
|
+
spin = spinner("Criando volume...");
|
|
3918
|
+
const volume = await client.volumes.create({
|
|
3919
|
+
serviceId: service.id,
|
|
3920
|
+
name,
|
|
3921
|
+
mountPath: opts.mount,
|
|
3922
|
+
sizeGb
|
|
3923
|
+
});
|
|
3924
|
+
spin.stop();
|
|
3925
|
+
updateServiceVolumesInConfig(key, (volumes) => [...volumes.filter((entry) => entry.name !== volume.name), {
|
|
3926
|
+
name: volume.name,
|
|
3927
|
+
mountPath: volume.mountPath,
|
|
3928
|
+
sizeGb: volume.sizeGb
|
|
3929
|
+
}]);
|
|
3930
|
+
success(`Volume ${chalk.bold(volume.name)} criado.`);
|
|
3931
|
+
if (opts.restart) {
|
|
3932
|
+
const syncSpin = spinner("Reiniciando serviço...");
|
|
3933
|
+
await client.volumes.syncDeployment({ serviceId: service.id });
|
|
3934
|
+
syncSpin.stop();
|
|
3935
|
+
success("Serviço reiniciando com os volumes atualizados.");
|
|
3936
|
+
} else await promptAndSyncDeployment(service.id);
|
|
3937
|
+
} catch (error) {
|
|
3938
|
+
spin?.stop();
|
|
3939
|
+
handleError(error);
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3942
|
+
volumesCommand.command("expand <volume>").alias("resize").description("Expandir um volume existente").requiredOption("--size <gb>", "Novo tamanho em GB").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (volumeRef, opts) => {
|
|
3943
|
+
let spin;
|
|
3944
|
+
try {
|
|
3945
|
+
const { key, service } = await resolveService(opts.service);
|
|
3946
|
+
const client = await getClient();
|
|
3947
|
+
const sizeGb = Number.parseInt(opts.size, 10);
|
|
3948
|
+
if (!Number.isInteger(sizeGb) || sizeGb < 1 || sizeGb > 100) throw new Error("O tamanho do volume deve ser um inteiro entre 1 e 100 GB.");
|
|
3949
|
+
const volume = await resolveRemoteVolume(service.id, volumeRef);
|
|
3950
|
+
spin = spinner("Expandindo volume...");
|
|
3951
|
+
await client.volumes.update({
|
|
3952
|
+
volumeId: volume.id,
|
|
3953
|
+
sizeGb
|
|
3954
|
+
});
|
|
3955
|
+
spin.stop();
|
|
3956
|
+
updateServiceVolumesInConfig(key, (volumes) => volumes.map((entry) => entry.name === volume.name ? {
|
|
3957
|
+
...entry,
|
|
3958
|
+
sizeGb
|
|
3959
|
+
} : entry));
|
|
3960
|
+
success(`Volume ${chalk.bold(volume.name)} expandido para ${sizeGb} GB.`);
|
|
3961
|
+
} catch (error) {
|
|
3962
|
+
spin?.stop();
|
|
3963
|
+
handleError(error);
|
|
3964
|
+
}
|
|
3965
|
+
});
|
|
3966
|
+
volumesCommand.command("delete <volume>").alias("deletar").description("Remover um volume do serviço (dados mantidos por 30 dias)").option("--service <service>", "Serviço alvo (chave ou nome)").option("--restart", "Reiniciar o serviço imediatamente após remover").option("--yes", "Pular confirmação").action(async (volumeRef, opts) => {
|
|
3967
|
+
let spin;
|
|
3968
|
+
try {
|
|
3969
|
+
const { key, service } = await resolveService(opts.service);
|
|
3970
|
+
const client = await getClient();
|
|
3971
|
+
const volume = await resolveRemoteVolume(service.id, volumeRef);
|
|
3972
|
+
if (!opts.yes) {
|
|
3973
|
+
if (!await promptConfirm(`Remover volume ${chalk.bold(volume.name)} (${volume.mountPath}) do serviço? Os dados serão mantidos por 30 dias.`, false)) {
|
|
3974
|
+
info("Operação cancelada.");
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
spin = spinner("Removendo volume...");
|
|
3979
|
+
await client.volumes.delete({ volumeId: volume.id });
|
|
3980
|
+
spin.stop();
|
|
3981
|
+
updateServiceVolumesInConfig(key, (volumes) => volumes.filter((entry) => entry.name !== volume.name));
|
|
3982
|
+
success(`Volume ${chalk.bold(volume.name)} removido do serviço.`);
|
|
3983
|
+
info("Os dados serão mantidos por 30 dias.");
|
|
3984
|
+
if (opts.restart) {
|
|
3985
|
+
const syncSpin = spinner("Reiniciando serviço...");
|
|
3986
|
+
await client.volumes.syncDeployment({ serviceId: service.id });
|
|
3987
|
+
syncSpin.stop();
|
|
3988
|
+
success("Serviço reiniciando sem o volume.");
|
|
3989
|
+
} else await promptAndSyncDeployment(service.id);
|
|
3990
|
+
} catch (error) {
|
|
3991
|
+
spin?.stop();
|
|
3992
|
+
handleError(error);
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
volumesCommand.command("sync").description("Reiniciar o serviço para aplicar alterações de volumes").option("--service <service>", "Serviço alvo (chave ou nome)").action(async (opts) => {
|
|
3996
|
+
let spin;
|
|
3997
|
+
try {
|
|
3998
|
+
const { service } = await resolveService(opts.service);
|
|
3999
|
+
const client = await getClient();
|
|
4000
|
+
spin = spinner("Sincronizando volumes...");
|
|
4001
|
+
await client.volumes.syncDeployment({ serviceId: service.id });
|
|
4002
|
+
spin.stop();
|
|
4003
|
+
success("Serviço reiniciando com os volumes atualizados.");
|
|
4004
|
+
} catch (error) {
|
|
4005
|
+
spin?.stop();
|
|
4006
|
+
handleError(error);
|
|
4007
|
+
}
|
|
4008
|
+
});
|
|
4009
|
+
|
|
3735
4010
|
//#endregion
|
|
3736
4011
|
//#region src/commands/config.ts
|
|
3737
4012
|
function formatValue(value) {
|
|
@@ -4057,7 +4332,7 @@ const whoamiCommand = new Command("whoami").description("Mostrar usuário autent
|
|
|
4057
4332
|
|
|
4058
4333
|
//#endregion
|
|
4059
4334
|
//#region src/index.ts
|
|
4060
|
-
const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.
|
|
4335
|
+
const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.14").option("--output <format>", "Formato de saída: fancy, json, github-actions, plain").option("--env <environment>", "Ambiente alvo (ex: preview, staging)").hook("preAction", (thisCommand) => {
|
|
4061
4336
|
const opts = thisCommand.opts();
|
|
4062
4337
|
if (opts.output) {
|
|
4063
4338
|
const valid = [
|
|
@@ -4082,6 +4357,7 @@ program.addCommand(deployCommand);
|
|
|
4082
4357
|
program.addCommand(logsCommand);
|
|
4083
4358
|
program.addCommand(envCommand);
|
|
4084
4359
|
program.addCommand(domainsCommand);
|
|
4360
|
+
program.addCommand(volumesCommand);
|
|
4085
4361
|
program.addCommand(configCommand);
|
|
4086
4362
|
program.addCommand(useCommand);
|
|
4087
4363
|
program.addCommand(apikeyCommand);
|