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.
Files changed (2) hide show
  1. package/dist/index.mjs +324 -48
  2. 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({ serviceId: service.serviceId }));
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.13";
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.13";
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
- if (serviceConf) await syncServiceConfig(client, serviceId, serviceConf);
2368
- const extraFiles = prepareExtraFiles(preDetection ?? detectLocalRepo(), serviceConf);
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({ serviceId }));
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 nestedPkg = readLocalFile(join(basePath, base, entry.name, "package.json"));
2495
- if (nestedPkg) files[`${base}/${entry.name}/package.json`] = nestedPkg;
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
- saveConfig({
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
- saveConfig({
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.13").option("--output <format>", "Formato de saída: fancy, json, github-actions, plain").option("--env <environment>", "Ambiente alvo (ex: preview, staging)").hook("preAction", (thisCommand) => {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onveloz",
3
- "version": "0.0.0-beta.13",
3
+ "version": "0.0.0-beta.14",
4
4
  "description": "CLI da plataforma Veloz — deploy rápido para o Brasil",
5
5
  "keywords": [
6
6
  "brasil",