onveloz 0.0.0-beta.5 → 0.0.0-beta.6

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.
Files changed (2) hide show
  1. package/dist/index.mjs +450 -1
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1393,6 +1393,427 @@ async function syncServiceConfig(client, serviceId, conf) {
1393
1393
  }));
1394
1394
  }
1395
1395
 
1396
+ //#endregion
1397
+ //#region src/lib/deploy-checks.ts
1398
+ /**
1399
+ * Platform-specific presets that won't work on Veloz (generic K8s).
1400
+ * Maps preset name to the platform it targets.
1401
+ */
1402
+ const INCOMPATIBLE_PRESETS = {
1403
+ vercel: "Vercel",
1404
+ "cloudflare-pages": "Cloudflare Pages",
1405
+ "cloudflare-workers": "Cloudflare Workers",
1406
+ "cloudflare-module": "Cloudflare Workers",
1407
+ netlify: "Netlify",
1408
+ "netlify-edge": "Netlify Edge",
1409
+ "aws-lambda": "AWS Lambda",
1410
+ "firebase": "Firebase",
1411
+ "deno-deploy": "Deno Deploy",
1412
+ "render-com": "Render",
1413
+ "flight-control": "Flightcontrol",
1414
+ "stormkit": "Stormkit",
1415
+ "edgio": "Edgio",
1416
+ "lagon": "Lagon"
1417
+ };
1418
+ /**
1419
+ * Recommended preset based on package manager / runtime.
1420
+ */
1421
+ function recommendedPreset(basePath) {
1422
+ const pkgPath = resolve(basePath, "package.json");
1423
+ if (!existsSync(pkgPath)) return "node-server";
1424
+ try {
1425
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1426
+ if ("bun" in {
1427
+ ...pkg.dependencies,
1428
+ ...pkg.devDependencies
1429
+ } || existsSync(resolve(basePath, "bun.lockb")) || existsSync(resolve(basePath, "bun.lock"))) return "bun";
1430
+ } catch {}
1431
+ return "node-server";
1432
+ }
1433
+ /**
1434
+ * Check for Nitro preset misconfigurations in vite/nuxt config files.
1435
+ */
1436
+ function checkNitroPreset(basePath) {
1437
+ for (const file of [
1438
+ "vite.config.ts",
1439
+ "vite.config.js",
1440
+ "vite.config.mjs",
1441
+ "nuxt.config.ts",
1442
+ "nuxt.config.js"
1443
+ ]) {
1444
+ const filePath = resolve(basePath, file);
1445
+ if (!existsSync(filePath)) continue;
1446
+ let content;
1447
+ try {
1448
+ content = readFileSync(filePath, "utf-8");
1449
+ } catch {
1450
+ continue;
1451
+ }
1452
+ const presetMatch = content.match(/preset\s*:\s*["']([^"']+)["']/);
1453
+ if (!presetMatch) continue;
1454
+ const preset = presetMatch[1];
1455
+ const platform$1 = INCOMPATIBLE_PRESETS[preset];
1456
+ if (!platform$1) continue;
1457
+ const recommended = recommendedPreset(basePath);
1458
+ return {
1459
+ message: `${file} usa preset "${preset}" (${platform$1}) — incompativel com Veloz`,
1460
+ hint: `Altere para preset: "${recommended}" em ${file}`
1461
+ };
1462
+ }
1463
+ return null;
1464
+ }
1465
+ /**
1466
+ * Check for Dockerfile COPY instructions that reference both bun.lockb and bun.lock.
1467
+ * Only one usually exists — the COPY will fail if both are listed but one is missing.
1468
+ */
1469
+ function checkDockerfileLockFiles(basePath) {
1470
+ const dockerfilePath = resolve(basePath, "Dockerfile");
1471
+ if (!existsSync(dockerfilePath)) return null;
1472
+ let content;
1473
+ try {
1474
+ content = readFileSync(dockerfilePath, "utf-8");
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ const copyLines = content.split("\n").filter((l) => /^COPY\s/.test(l.trim()));
1479
+ for (const line of copyLines) if (line.includes("bun.lockb") && line.includes("bun.lock") && !line.includes("bun.lock*")) {
1480
+ if (!(existsSync(resolve(basePath, "bun.lockb")) && existsSync(resolve(basePath, "bun.lock")))) return {
1481
+ message: "Dockerfile lista bun.lockb e bun.lock mas apenas um existe",
1482
+ hint: "Use \"COPY package.json bun.lock* ./\" para copiar o que existir"
1483
+ };
1484
+ }
1485
+ return null;
1486
+ }
1487
+ /**
1488
+ * Next.js needs `output: "standalone"` for Docker/K8s deploys.
1489
+ * Without it, the build produces a node_modules-dependent output
1490
+ * that's huge and doesn't run well in containers.
1491
+ */
1492
+ function checkNextStandalone(basePath) {
1493
+ const pkgPath = resolve(basePath, "package.json");
1494
+ if (!existsSync(pkgPath)) return null;
1495
+ try {
1496
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1497
+ if (!("next" in {
1498
+ ...pkg.dependencies,
1499
+ ...pkg.devDependencies
1500
+ })) return null;
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1505
+ for (const file of [
1506
+ "next.config.ts",
1507
+ "next.config.js",
1508
+ "next.config.mjs"
1509
+ ]) {
1510
+ const filePath = resolve(basePath, file);
1511
+ if (!existsSync(filePath)) continue;
1512
+ try {
1513
+ const content = readFileSync(filePath, "utf-8");
1514
+ if (content.includes("\"standalone\"") || content.includes("'standalone'")) return null;
1515
+ return {
1516
+ message: `${file} nao tem output: "standalone"`,
1517
+ hint: "Adicione output: \"standalone\" no next.config para deploys em container"
1518
+ };
1519
+ } catch {
1520
+ continue;
1521
+ }
1522
+ }
1523
+ return null;
1524
+ }
1525
+ /**
1526
+ * Prisma needs `prisma generate` in the build step.
1527
+ * Without it, the Prisma client won't be generated and the app will crash.
1528
+ */
1529
+ function checkPrismaGenerate(basePath) {
1530
+ const pkgPath = resolve(basePath, "package.json");
1531
+ if (!existsSync(pkgPath)) return null;
1532
+ try {
1533
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1534
+ const allDeps = {
1535
+ ...pkg.dependencies,
1536
+ ...pkg.devDependencies
1537
+ };
1538
+ if (!("prisma" in allDeps) && !("@prisma/client" in allDeps)) return null;
1539
+ const scripts = pkg.scripts || {};
1540
+ const buildScript = scripts.build || "";
1541
+ const postinstall = scripts.postinstall || "";
1542
+ if (buildScript.includes("prisma generate") || postinstall.includes("prisma generate") || buildScript.includes("prisma db push")) return null;
1543
+ return {
1544
+ message: "Prisma detectado mas prisma generate nao esta no build/postinstall",
1545
+ hint: "Adicione \"prisma generate\" ao script postinstall ou build no package.json"
1546
+ };
1547
+ } catch {
1548
+ return null;
1549
+ }
1550
+ }
1551
+ /**
1552
+ * Detect if the app hardcodes a port that doesn't match the configured port.
1553
+ * Common issue: app listens on 8080 but service port is 3000.
1554
+ */
1555
+ function checkPortMismatch(basePath) {
1556
+ const pkgPath = resolve(basePath, "package.json");
1557
+ if (!existsSync(pkgPath)) return null;
1558
+ try {
1559
+ const portMatch = ((JSON.parse(readFileSync(pkgPath, "utf-8")).scripts || {}).start || "").match(/(?:--port|-p)\s+(\d+)/);
1560
+ if (portMatch) {
1561
+ const hardcodedPort = parseInt(portMatch[1], 10);
1562
+ if (hardcodedPort !== 3e3) return {
1563
+ message: `Script start usa porta ${hardcodedPort} — certifique-se de que a porta do servico esta configurada corretamente`,
1564
+ hint: `Configure a porta do servico para ${hardcodedPort} no dashboard ou veloz.json`
1565
+ };
1566
+ }
1567
+ } catch {}
1568
+ return null;
1569
+ }
1570
+ /**
1571
+ * Check for .env files that might be accidentally uploaded.
1572
+ */
1573
+ function checkEnvFileCommitted(basePath) {
1574
+ if (!existsSync(resolve(basePath, ".env"))) return null;
1575
+ const gitignorePath = resolve(basePath, ".gitignore");
1576
+ if (existsSync(gitignorePath)) try {
1577
+ const lines = readFileSync(gitignorePath, "utf-8").split("\n").map((l) => l.trim());
1578
+ if (lines.includes(".env") || lines.includes(".env*") || lines.includes("*.env")) return null;
1579
+ } catch {}
1580
+ return {
1581
+ message: "Arquivo .env encontrado e nao esta no .gitignore",
1582
+ hint: "Adicione .env ao .gitignore — use variaveis de ambiente no dashboard ou CLI"
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Node.js project without a start command — nixpacks won't know how to run it.
1587
+ * Checks for: scripts.start, main field, or common entry files.
1588
+ */
1589
+ function checkMissingStartCommand(basePath) {
1590
+ const pkgPath = resolve(basePath, "package.json");
1591
+ if (!existsSync(pkgPath)) return null;
1592
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1593
+ try {
1594
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1595
+ if (pkg.scripts?.start) return null;
1596
+ if (pkg.main) return null;
1597
+ const allDeps = {
1598
+ ...pkg.dependencies,
1599
+ ...pkg.devDependencies
1600
+ };
1601
+ if ([
1602
+ "next",
1603
+ "nuxt",
1604
+ "nuxt3",
1605
+ "@sveltejs/kit",
1606
+ "remix",
1607
+ "astro",
1608
+ "@angular/core",
1609
+ "gatsby"
1610
+ ].some((f) => f in allDeps)) return null;
1611
+ if ([
1612
+ "index.js",
1613
+ "index.mjs",
1614
+ "index.ts",
1615
+ "server.js",
1616
+ "server.ts",
1617
+ "app.js",
1618
+ "app.ts",
1619
+ "src/index.js",
1620
+ "src/index.ts",
1621
+ "src/server.js",
1622
+ "src/server.ts"
1623
+ ].some((f) => existsSync(resolve(basePath, f)))) return null;
1624
+ return {
1625
+ message: "Nenhum script start encontrado no package.json",
1626
+ hint: "Adicione \"start\" em scripts (ex: \"node dist/index.js\") ou um campo \"main\" no package.json"
1627
+ };
1628
+ } catch {
1629
+ return null;
1630
+ }
1631
+ }
1632
+ /**
1633
+ * packageManager field in package.json doesn't match the lockfile present.
1634
+ * e.g., packageManager: "pnpm@9.0.0" but only package-lock.json exists.
1635
+ */
1636
+ function checkPackageManagerMismatch(basePath) {
1637
+ const pkgPath = resolve(basePath, "package.json");
1638
+ if (!existsSync(pkgPath)) return null;
1639
+ try {
1640
+ const pmField = JSON.parse(readFileSync(pkgPath, "utf-8")).packageManager;
1641
+ if (!pmField) return null;
1642
+ const declaredPm = pmField.split("@")[0];
1643
+ const lockfileMap = {
1644
+ npm: ["package-lock.json"],
1645
+ yarn: ["yarn.lock"],
1646
+ pnpm: ["pnpm-lock.yaml"],
1647
+ bun: ["bun.lockb", "bun.lock"]
1648
+ };
1649
+ const expectedLockfiles = lockfileMap[declaredPm];
1650
+ if (!expectedLockfiles) return null;
1651
+ if (expectedLockfiles.some((f) => existsSync(resolve(basePath, f)))) return null;
1652
+ const otherPms = Object.entries(lockfileMap).filter(([pm]) => pm !== declaredPm);
1653
+ for (const [pm, files] of otherPms) if (files.some((f) => existsSync(resolve(basePath, f)))) return {
1654
+ message: `packageManager "${pmField}" no package.json mas lockfile de ${pm} encontrado`,
1655
+ hint: `Remova o campo packageManager ou gere o lockfile correto com ${declaredPm} install`
1656
+ };
1657
+ } catch {}
1658
+ return null;
1659
+ }
1660
+ /**
1661
+ * Native modules that need system packages to build.
1662
+ * The server auto-detects and injects apt packages for known modules (sharp, canvas,
1663
+ * puppeteer, playwright-chromium). This check warns about bcrypt which has a pure-JS
1664
+ * alternative, and modules not in the auto-detect list.
1665
+ */
1666
+ function checkNativeModules(basePath) {
1667
+ const pkgPath = resolve(basePath, "package.json");
1668
+ if (!existsSync(pkgPath)) return null;
1669
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1670
+ try {
1671
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1672
+ if ("bcrypt" in {
1673
+ ...pkg.dependencies,
1674
+ ...pkg.devDependencies
1675
+ }) return {
1676
+ message: "bcrypt compila codigo nativo — pode falhar em alguns ambientes",
1677
+ hint: "Considere usar bcryptjs (pure JS) para evitar falhas de build"
1678
+ };
1679
+ } catch {}
1680
+ return null;
1681
+ }
1682
+ /**
1683
+ * nixpacks.toml phases that override defaults instead of extending them.
1684
+ * Missing "..." in [phases.X.cmds] replaces all default commands.
1685
+ */
1686
+ function checkNixpacksTomlSpread(basePath) {
1687
+ const tomlPath = resolve(basePath, "nixpacks.toml");
1688
+ if (!existsSync(tomlPath)) return null;
1689
+ try {
1690
+ const content = readFileSync(tomlPath, "utf-8");
1691
+ if (!content.match(/\[phases\.\w+\]/g)) return null;
1692
+ const hasCmds = /cmds\s*=\s*\[/.test(content);
1693
+ const hasSpread = content.includes("\"...\"");
1694
+ if (hasCmds && !hasSpread) return {
1695
+ message: "nixpacks.toml define cmds sem \"...\" — isso substitui os comandos padrao",
1696
+ hint: "Adicione \"...\" no array cmds para manter os comandos padrao: cmds = [\"...\", \"seu-comando\"]"
1697
+ };
1698
+ } catch {}
1699
+ return null;
1700
+ }
1701
+ /**
1702
+ * Django project without gunicorn — the dev server isn't suitable for production.
1703
+ */
1704
+ function checkDjangoGunicorn(basePath) {
1705
+ if (existsSync(resolve(basePath, "Dockerfile"))) return null;
1706
+ const requirementsFiles = [
1707
+ "requirements.txt",
1708
+ "requirements/production.txt",
1709
+ "requirements/prod.txt"
1710
+ ];
1711
+ let hasDjango = false;
1712
+ let hasGunicorn = false;
1713
+ for (const file of requirementsFiles) {
1714
+ const filePath = resolve(basePath, file);
1715
+ if (!existsSync(filePath)) continue;
1716
+ try {
1717
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
1718
+ if (content.includes("django")) hasDjango = true;
1719
+ if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
1720
+ } catch {
1721
+ continue;
1722
+ }
1723
+ }
1724
+ for (const file of ["pyproject.toml", "Pipfile"]) {
1725
+ const filePath = resolve(basePath, file);
1726
+ if (!existsSync(filePath)) continue;
1727
+ try {
1728
+ const content = readFileSync(filePath, "utf-8").toLowerCase();
1729
+ if (content.includes("django")) hasDjango = true;
1730
+ if (content.includes("gunicorn") || content.includes("uvicorn")) hasGunicorn = true;
1731
+ } catch {
1732
+ continue;
1733
+ }
1734
+ }
1735
+ if (hasDjango && !hasGunicorn) return {
1736
+ message: "Django detectado sem gunicorn/uvicorn — o servidor de dev nao deve ser usado em producao",
1737
+ hint: "Adicione gunicorn ao requirements.txt e configure o start command: \"gunicorn myproject.wsgi\""
1738
+ };
1739
+ return null;
1740
+ }
1741
+ /**
1742
+ * SvelteKit needs adapter-node for container deploys.
1743
+ * Default adapter-auto or adapter-vercel/netlify won't work.
1744
+ */
1745
+ function checkSvelteKitAdapter(basePath) {
1746
+ const pkgPath = resolve(basePath, "package.json");
1747
+ if (!existsSync(pkgPath)) return null;
1748
+ try {
1749
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1750
+ const allDeps = {
1751
+ ...pkg.dependencies,
1752
+ ...pkg.devDependencies
1753
+ };
1754
+ if (!("@sveltejs/kit" in allDeps)) return null;
1755
+ const hasNodeAdapter = "@sveltejs/adapter-node" in allDeps;
1756
+ const hasBunAdapter = "svelte-adapter-bun" in allDeps;
1757
+ if (hasNodeAdapter || hasBunAdapter) return null;
1758
+ const installed = [
1759
+ "@sveltejs/adapter-vercel",
1760
+ "@sveltejs/adapter-netlify",
1761
+ "@sveltejs/adapter-cloudflare",
1762
+ "@sveltejs/adapter-cloudflare-workers"
1763
+ ].find((a) => a in allDeps);
1764
+ if (installed) return {
1765
+ message: `SvelteKit usa ${installed} — incompativel com Veloz`,
1766
+ hint: "Instale @sveltejs/adapter-node e configure em svelte.config.js"
1767
+ };
1768
+ if ("@sveltejs/adapter-auto" in allDeps) return {
1769
+ message: "SvelteKit usa adapter-auto — pode nao funcionar em container",
1770
+ hint: "Instale @sveltejs/adapter-node para deploys em container"
1771
+ };
1772
+ } catch {}
1773
+ return null;
1774
+ }
1775
+ /**
1776
+ * Run all pre-deploy checks and return warnings.
1777
+ * Does not block — just warns the user.
1778
+ */
1779
+ function runPreDeployChecks(basePath = ".") {
1780
+ const warnings = [];
1781
+ const fullPath = resolve(process.cwd(), basePath);
1782
+ const checks = [
1783
+ checkNitroPreset,
1784
+ checkDockerfileLockFiles,
1785
+ checkNextStandalone,
1786
+ checkPrismaGenerate,
1787
+ checkPortMismatch,
1788
+ checkEnvFileCommitted,
1789
+ checkSvelteKitAdapter,
1790
+ checkMissingStartCommand,
1791
+ checkPackageManagerMismatch,
1792
+ checkNativeModules,
1793
+ checkNixpacksTomlSpread,
1794
+ checkDjangoGunicorn
1795
+ ];
1796
+ for (const check of checks) {
1797
+ const result = check(fullPath);
1798
+ if (result) warnings.push(result);
1799
+ }
1800
+ return warnings;
1801
+ }
1802
+ /**
1803
+ * Print deploy warnings to the console.
1804
+ * Returns true if any warnings were found.
1805
+ */
1806
+ function printDeployWarnings(warnings) {
1807
+ if (warnings.length === 0) return false;
1808
+ console.log();
1809
+ for (const w of warnings) {
1810
+ console.log(chalk.yellow(` AVISO: ${w.message}`));
1811
+ console.log(chalk.dim(` ${w.hint}`));
1812
+ }
1813
+ console.log();
1814
+ return true;
1815
+ }
1816
+
1396
1817
  //#endregion
1397
1818
  //#region src/commands/deploy.ts
1398
1819
  /**
@@ -1414,6 +1835,24 @@ async function computeExtraFilesForServices(services) {
1414
1835
  const velozConfig = loadConfig();
1415
1836
  const client = await getClient();
1416
1837
  const results = [];
1838
+ const allWarnings = [];
1839
+ for (const svc of services) {
1840
+ const warnings = runPreDeployChecks(resolveServiceConf(velozConfig, svc.serviceId)?.rootDirectory || ".");
1841
+ if (warnings.length > 0) allWarnings.push({
1842
+ service: svc.serviceName,
1843
+ warnings
1844
+ });
1845
+ }
1846
+ if (allWarnings.length > 0) {
1847
+ for (const { service, warnings } of allWarnings) {
1848
+ console.log(chalk.yellow(`\n ${chalk.bold(service)}:`));
1849
+ printDeployWarnings(warnings);
1850
+ }
1851
+ if (!await promptConfirm("Continuar mesmo assim?", false)) {
1852
+ info("Deploy cancelado.");
1853
+ process.exit(0);
1854
+ }
1855
+ }
1417
1856
  for (const svc of services) {
1418
1857
  const serviceConf = resolveServiceConf(velozConfig, svc.serviceId);
1419
1858
  if (serviceConf) await syncServiceConfig(client, svc.serviceId, serviceConf);
@@ -1435,6 +1874,16 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
1435
1874
  const serviceConf = resolveServiceConf(loadConfig(), serviceId);
1436
1875
  if (serviceConf) await syncServiceConfig(client, serviceId, serviceConf);
1437
1876
  const extraFiles = prepareExtraFiles(preDetection ?? detectLocalRepo(), serviceConf);
1877
+ const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
1878
+ if (warnings.length > 0) {
1879
+ spinUpload.stop();
1880
+ printDeployWarnings(warnings);
1881
+ if (!await promptConfirm("Continuar mesmo assim?", false)) {
1882
+ info("Deploy cancelado.");
1883
+ return;
1884
+ }
1885
+ spinUpload.start();
1886
+ }
1438
1887
  spinUpload.text = "Iniciando deploy...";
1439
1888
  const deployment = await withRetry(() => client.deployments.create({ serviceId }));
1440
1889
  spinUpload.text = "Fazendo upload do código...";
@@ -2704,7 +3153,7 @@ const whoamiCommand = new Command("whoami").description("Mostrar usuário autent
2704
3153
 
2705
3154
  //#endregion
2706
3155
  //#region src/index.ts
2707
- const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.5");
3156
+ const program = new Command().name("veloz").description("CLI da plataforma Veloz — deploy rápido para o Brasil").version("0.0.0-beta.6");
2708
3157
  program.addCommand(loginCommand);
2709
3158
  program.addCommand(logoutCommand);
2710
3159
  program.addCommand(projectsCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onveloz",
3
- "version": "0.0.0-beta.5",
3
+ "version": "0.0.0-beta.6",
4
4
  "description": "CLI da plataforma Veloz — deploy rápido para o Brasil",
5
5
  "type": "module",
6
6
  "license": "MIT",