onveloz 0.0.0-beta.15 → 0.0.0-beta.16

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 +474 -82
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -29,7 +29,6 @@ const BUILD_TIMEOUT_MS = 600 * 1e3;
29
29
  const DEPLOY_TIMEOUT_MS = 300 * 1e3;
30
30
  const DATABASE_PROVISION_TIMEOUT_MS = 300 * 1e3;
31
31
  const DATABASE_WAITING_ON_PROVIDER_AFTER_MS = 60 * 1e3;
32
- const DATABASE_HEALTH_POLL_INTERVAL_MS = 60 * 1e3;
33
32
  const DATABASE_ENGINES = [
34
33
  "postgresql",
35
34
  "mysql",
@@ -52,7 +51,6 @@ const DATABASE_ENGINE_DEFAULTS = {
52
51
  defaultVersion: "7"
53
52
  }
54
53
  };
55
- const DEFAULT_SLEEP_CHECK_INTERVAL_MS = 300 * 1e3;
56
54
 
57
55
  //#endregion
58
56
  //#region ../../packages/config/veloz-config.ts
@@ -111,6 +109,20 @@ const VolumeConfigSchema = z$1.object({
111
109
  ].some((p) => value === p || value.startsWith(p + "/")), "Caminho de montagem não permitido por segurança"),
112
110
  sizeGb: z$1.number().int().min(10).max(100).optional().default(10)
113
111
  });
112
+ const DatabaseResourcesSchema = z$1.object({
113
+ cpu: z$1.string().regex(/^[0-9]+(\.[0-9]+)?|[0-9]+m$/).default("500m").optional(),
114
+ memory: z$1.string().regex(/^[0-9]+(Mi|Gi)$/).default("512Mi").optional()
115
+ });
116
+ const PoolerConfigSchema = z$1.object({
117
+ enabled: z$1.boolean().default(false),
118
+ poolMode: z$1.enum([
119
+ "transaction",
120
+ "session",
121
+ "statement"
122
+ ]).default("transaction").optional(),
123
+ defaultPoolSize: z$1.number().int().min(1).max(200).default(20).optional(),
124
+ maxClientConn: z$1.number().int().min(1).max(1e4).default(100).optional()
125
+ });
114
126
  const DatabaseConfigSchema = z$1.object({
115
127
  id: z$1.string().optional(),
116
128
  name: z$1.string().optional(),
@@ -121,6 +133,8 @@ const DatabaseConfigSchema = z$1.object({
121
133
  ]),
122
134
  version: z$1.string().optional(),
123
135
  storage: z$1.string().regex(/^[0-9]+(Gi)$/).default("10Gi").optional(),
136
+ resources: DatabaseResourcesSchema.optional(),
137
+ pooler: PoolerConfigSchema.optional(),
124
138
  fromTemplate: z$1.string().optional()
125
139
  });
126
140
  const ServiceConfigSchema = z$1.object({
@@ -542,7 +556,9 @@ async function pollForToken(authClient, deviceCode, interval) {
542
556
  let pollingInterval = interval;
543
557
  const maxAttempts = Math.ceil(300 / pollingInterval);
544
558
  for (let i = 0; i < maxAttempts; i++) {
545
- await new Promise((r) => setTimeout(r, pollingInterval * 1e3));
559
+ await new Promise((r) => {
560
+ setTimeout(r, pollingInterval * 1e3);
561
+ });
546
562
  try {
547
563
  const { data, error } = await authClient.device.token({
548
564
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
@@ -838,6 +854,7 @@ async function resolveService(serviceFlag) {
838
854
  }
839
855
  async function resolveServiceId(serviceFlag) {
840
856
  const { service } = await resolveService(serviceFlag);
857
+ if (!service.id) throw new Error(`Serviço "${service.name}" não possui ID. Execute 'veloz deploy' para vincular o serviço.`);
841
858
  return service.id;
842
859
  }
843
860
  function resolveAllServices(serviceFlag) {
@@ -1847,7 +1864,10 @@ dbGroup.command("create", {
1847
1864
  name: z.string().optional().describe("Nome do banco de dados"),
1848
1865
  engine: z.string().optional().describe("Engine (postgresql, mysql, redis)"),
1849
1866
  engineVersion: z.string().optional().describe("Versão do engine"),
1850
- storage: z.string().optional().describe("Armazenamento (ex: 10Gi, 20Gi)")
1867
+ storage: z.string().optional().describe("Armazenamento (ex: 10Gi, 20Gi)"),
1868
+ cpu: z.string().optional().describe("Limite de CPU (ex: 500m, 1)"),
1869
+ memory: z.string().optional().describe("Limite de memória (ex: 512Mi, 1Gi)"),
1870
+ pooler: z.boolean().optional().describe("Habilitar PgBouncer (apenas PostgreSQL)")
1851
1871
  }),
1852
1872
  async run(c) {
1853
1873
  const projectId = getProjectId$1();
@@ -1896,7 +1916,10 @@ dbGroup.command("create", {
1896
1916
  name,
1897
1917
  engine: validEngine,
1898
1918
  engineVersion: version,
1899
- storage
1919
+ storage,
1920
+ cpuLimit: c.options.cpu,
1921
+ memoryLimit: c.options.memory,
1922
+ poolerEnabled: c.options.pooler
1900
1923
  })
1901
1924
  });
1902
1925
  success(`Banco de dados ${chalk.bold(db.name)} criado! Provisionando...`);
@@ -1908,7 +1931,12 @@ dbGroup.command("create", {
1908
1931
  id: db.id,
1909
1932
  engine: validEngine,
1910
1933
  version: version ?? void 0,
1911
- storage: storage ?? void 0
1934
+ storage: storage ?? void 0,
1935
+ ...c.options.cpu || c.options.memory ? { resources: {
1936
+ ...c.options.cpu && { cpu: c.options.cpu },
1937
+ ...c.options.memory && { memory: c.options.memory }
1938
+ } } : {},
1939
+ ...c.options.pooler ? { pooler: { enabled: true } } : {}
1912
1940
  };
1913
1941
  config.databases = updatedDatabases;
1914
1942
  config.updated = (/* @__PURE__ */ new Date()).toISOString();
@@ -2453,7 +2481,7 @@ function analyzeRepo(files) {
2453
2481
  name: appName,
2454
2482
  path: appPath,
2455
2483
  framework: appFramework,
2456
- usesNodeFs: sourceEntries.some(([filePath$1, fileContent]) => filePath$1.startsWith(`${appPath}/`) && usesNodeFs(fileContent))
2484
+ usesNodeFs: sourceEntries.some(([srcPath, fileContent]) => srcPath.startsWith(`${appPath}/`) && usesNodeFs(fileContent))
2457
2485
  });
2458
2486
  }
2459
2487
  return {
@@ -2610,10 +2638,14 @@ async function withRetry(fn, maxRetries = 3) {
2610
2638
  const rateLimit = isRateLimitError(error);
2611
2639
  if (rateLimit) {
2612
2640
  const waitMs = Math.min(rateLimit.retryAfterMs, 3e4);
2613
- await new Promise((r) => setTimeout(r, waitMs));
2641
+ await new Promise((r) => {
2642
+ setTimeout(r, waitMs);
2643
+ });
2614
2644
  } else {
2615
2645
  const delay = Math.min(1e3 * Math.pow(2, attempt), 1e4);
2616
- await new Promise((r) => setTimeout(r, delay));
2646
+ await new Promise((r) => {
2647
+ setTimeout(r, delay);
2648
+ });
2617
2649
  }
2618
2650
  }
2619
2651
  throw new Error("Max retries exceeded");
@@ -2623,7 +2655,7 @@ async function withRetry(fn, maxRetries = 3) {
2623
2655
  //#region src/lib/deploy-constants.ts
2624
2656
  const statusLabels = {
2625
2657
  QUEUED: "Na fila",
2626
- BUILDING: "Construindo",
2658
+ BUILDING: "Compilando",
2627
2659
  BUILD_FAILED: "Falha na construção",
2628
2660
  DEPLOYING: "Implantando",
2629
2661
  LIVE: "Ativo",
@@ -2724,8 +2756,7 @@ function renderProgress(progressMap, prevLineCount) {
2724
2756
  if (nonEmptyLines.length > 0) {
2725
2757
  const tail = nonEmptyLines.slice(-3);
2726
2758
  for (const line of tail) {
2727
- const truncated = line.substring(0, 80) + (line.length > 80 ? "..." : "");
2728
- process.stdout.write(` ${chalk.dim(truncated)}\n`);
2759
+ process.stdout.write(` ${chalk.dim(line)}\n`);
2729
2760
  lineCount++;
2730
2761
  }
2731
2762
  } else if (progress.status === "BUILDING") {
@@ -2811,7 +2842,9 @@ async function deployServicesInParallel(services) {
2811
2842
  if (isTTY) lineCount = renderProgress(progressMap, lineCount);
2812
2843
  const streamPromises = activeDeployments.map(async ({ service, deploymentId }) => {
2813
2844
  try {
2814
- await new Promise((resolve$1) => setTimeout(resolve$1, 1e3));
2845
+ await new Promise((resolve$1) => {
2846
+ setTimeout(resolve$1, 1e3);
2847
+ });
2815
2848
  const stream = await client.logs.streamBuildLogs({ deploymentId });
2816
2849
  for await (const event of stream) {
2817
2850
  const progress = progressMap.get(service.serviceId);
@@ -2960,6 +2993,310 @@ function getFailureHints(status) {
2960
2993
  default: return ["Execute 'veloz logs -f' para mais detalhes."];
2961
2994
  }
2962
2995
  }
2996
+ /** Raw BuildKit line: `#N content` */
2997
+ const BUILDKIT_PREFIX_RE = /^#(\d+)\s+(.*)/;
2998
+ /** Docker build step: `[stage step/total] COMMAND` */
2999
+ const DOCKER_STEP_RE = /^\[(\S+)\s+(\d+)\/(\d+)\]\s+(.+)$/;
3000
+ /** Platform timestamp: `[2026-03-23T02:37:13.795Z] message` */
3001
+ const TIMESTAMP_RE = /^\[(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\]\s+(.+)$/;
3002
+ /** DONE marker: `DONE 4.2s` */
3003
+ const DONE_RE = /^DONE\s+([\d.]+s?)$/;
3004
+ /** Timing prefix: `0.543 actual content` */
3005
+ const TIMING_RE = /^(\d+\.\d+)\s+(.*)/;
3006
+ /** Infrastructure / deploy orchestration messages to hide from user output */
3007
+ const HIDDEN_MESSAGES = [
3008
+ /^Ensuring namespace\b/i,
3009
+ /^Updating deployment\b/i,
3010
+ /^Syncing ingress\b/i,
3011
+ /^Waiting for rollout\b/i,
3012
+ /^Deploy complete\b/i,
3013
+ /^→\s+https?:\/\//,
3014
+ /\bnamespace\b.*\bsvc\.cluster\.local\b/i,
3015
+ /\bpod\b/i,
3016
+ /\bkubernetes\b/i,
3017
+ /\bk8s\b/i,
3018
+ /\brollout\b/i
3019
+ ];
3020
+ function isHiddenMessage(text) {
3021
+ return HIDDEN_MESSAGES.some((p) => p.test(text));
3022
+ }
3023
+ /**
3024
+ * Try to extract a human-readable message from a structured JSON log line.
3025
+ * Returns null if the line is not JSON or has no message.
3026
+ */
3027
+ function parseJsonLog(text) {
3028
+ if (!text.startsWith("{")) return null;
3029
+ try {
3030
+ const parsed = JSON.parse(text);
3031
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.msg === "string") return parsed.msg;
3032
+ } catch {}
3033
+ return null;
3034
+ }
3035
+ /** Clean a display line — parse JSON, filter infra */
3036
+ function cleanDisplayLine(text) {
3037
+ if (isHiddenMessage(text)) return null;
3038
+ const jsonMsg = parseJsonLog(text);
3039
+ if (jsonMsg !== null) {
3040
+ if (isHiddenMessage(jsonMsg)) return null;
3041
+ return jsonMsg;
3042
+ }
3043
+ return text;
3044
+ }
3045
+ function parseBuildLine(raw) {
3046
+ const trimmed = raw.trim();
3047
+ const bkMatch = BUILDKIT_PREFIX_RE.exec(trimmed);
3048
+ if (bkMatch) {
3049
+ const stepNum = parseInt(bkMatch[1], 10);
3050
+ const content = bkMatch[2];
3051
+ if (content.trim() === "CACHED") return {
3052
+ kind: "cached",
3053
+ stepNum
3054
+ };
3055
+ const doneMatch = DONE_RE.exec(content);
3056
+ if (doneMatch) return {
3057
+ kind: "done",
3058
+ stepNum,
3059
+ duration: doneMatch[1]
3060
+ };
3061
+ const stepMatch = DOCKER_STEP_RE.exec(content);
3062
+ if (stepMatch) return {
3063
+ kind: "step",
3064
+ stepNum,
3065
+ stage: stepMatch[1],
3066
+ step: parseInt(stepMatch[2], 10),
3067
+ total: parseInt(stepMatch[3], 10),
3068
+ command: stepMatch[4]
3069
+ };
3070
+ const timingMatch = TIMING_RE.exec(content);
3071
+ if (timingMatch) {
3072
+ const text = timingMatch[2];
3073
+ if (text.trim()) return {
3074
+ kind: "output",
3075
+ stepNum,
3076
+ text
3077
+ };
3078
+ return {
3079
+ kind: "other",
3080
+ text: ""
3081
+ };
3082
+ }
3083
+ if (content.trim()) return {
3084
+ kind: "output",
3085
+ stepNum,
3086
+ text: content
3087
+ };
3088
+ return {
3089
+ kind: "other",
3090
+ text: ""
3091
+ };
3092
+ }
3093
+ const tsMatch = TIMESTAMP_RE.exec(trimmed);
3094
+ if (tsMatch) return {
3095
+ kind: "platform",
3096
+ message: tsMatch[2]
3097
+ };
3098
+ return {
3099
+ kind: "other",
3100
+ text: trimmed
3101
+ };
3102
+ }
3103
+ const BAR_WIDTH = 20;
3104
+ const BRAND = chalk.rgb(255, 77, 0);
3105
+ function renderProgressBar(filled, total, allCached, allDone) {
3106
+ const ratio = total > 0 ? filled / total : 0;
3107
+ const filledChars = Math.round(ratio * BAR_WIDTH);
3108
+ const emptyChars = BAR_WIDTH - filledChars;
3109
+ const counter = `${filled}/${total}`;
3110
+ if (allCached) return `${BRAND("━".repeat(BAR_WIDTH))} ${BRAND(`${counter} ◆ cached`)}`;
3111
+ if (allDone) return `${chalk.green("━".repeat(BAR_WIDTH))} ${chalk.green(`${counter} ✓`)}`;
3112
+ return chalk.cyan("━".repeat(filledChars)) + chalk.dim("─".repeat(emptyChars)) + ` ${chalk.dim(counter)}`;
3113
+ }
3114
+ const SPINNER_FRAMES = [
3115
+ "⠋",
3116
+ "⠙",
3117
+ "⠹",
3118
+ "⠸",
3119
+ "⠼",
3120
+ "⠴",
3121
+ "⠦",
3122
+ "⠧",
3123
+ "⠇",
3124
+ "⠏"
3125
+ ];
3126
+ /**
3127
+ * Dashboard-style renderer for compact TTY mode.
3128
+ * Redraws the entire build progress block on each update.
3129
+ * Includes an integrated spinner that animates via setInterval.
3130
+ */
3131
+ var BuildProgressRenderer = class {
3132
+ stages = /* @__PURE__ */ new Map();
3133
+ stageOrder = [];
3134
+ platformMessages = [];
3135
+ renderLineCount = 0;
3136
+ phase = "waiting";
3137
+ runtimeHeaderPrinted = false;
3138
+ serviceName;
3139
+ spinnerFrame = 0;
3140
+ spinnerInterval = null;
3141
+ spinnerText = "Aguardando início do build...";
3142
+ /** External ora spinner reference — used to pause/resume during runtime log output */
3143
+ externalSpinner = null;
3144
+ constructor(serviceName) {
3145
+ this.serviceName = serviceName;
3146
+ this.startSpinner();
3147
+ }
3148
+ setExternalSpinner(spinner$1) {
3149
+ this.externalSpinner = spinner$1;
3150
+ }
3151
+ startSpinner() {
3152
+ if (this.spinnerInterval) return;
3153
+ this.spinnerInterval = setInterval(() => {
3154
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
3155
+ this.render();
3156
+ }, 80);
3157
+ }
3158
+ stopSpinner() {
3159
+ if (this.spinnerInterval) {
3160
+ clearInterval(this.spinnerInterval);
3161
+ this.spinnerInterval = null;
3162
+ }
3163
+ }
3164
+ setBuilding() {
3165
+ this.phase = "building";
3166
+ this.spinnerText = "Compilando...";
3167
+ }
3168
+ switchToRuntime() {
3169
+ if (this.phase === "runtime") return;
3170
+ for (const stage of this.stages.values()) if (stage.steps.size > 0 && stage.steps.size < stage.total) stage.total = stage.steps.size;
3171
+ this.stopSpinner();
3172
+ this.render();
3173
+ this.renderLineCount = 0;
3174
+ this.phase = "runtime";
3175
+ }
3176
+ processLine(raw) {
3177
+ const trimmed = raw.trim();
3178
+ if (!trimmed) return;
3179
+ if (this.phase === "runtime") {
3180
+ this.printRuntimeLine(trimmed);
3181
+ return;
3182
+ }
3183
+ if (this.phase === "waiting") {
3184
+ this.phase = "building";
3185
+ this.spinnerText = "Compilando...";
3186
+ }
3187
+ const parsed = parseBuildLine(trimmed);
3188
+ switch (parsed.kind) {
3189
+ case "step": {
3190
+ let stage = this.stages.get(parsed.stage);
3191
+ if (!stage) {
3192
+ stage = {
3193
+ name: parsed.stage,
3194
+ total: parsed.total,
3195
+ steps: /* @__PURE__ */ new Map(),
3196
+ stepNumMap: /* @__PURE__ */ new Map(),
3197
+ cachedStepNums: /* @__PURE__ */ new Set(),
3198
+ doneStepNums: /* @__PURE__ */ new Set()
3199
+ };
3200
+ this.stages.set(parsed.stage, stage);
3201
+ this.stageOrder.push(parsed.stage);
3202
+ }
3203
+ stage.steps.set(parsed.step, parsed.command);
3204
+ stage.stepNumMap.set(parsed.stepNum, parsed.step);
3205
+ stage.total = Math.max(stage.total, parsed.total);
3206
+ break;
3207
+ }
3208
+ case "cached":
3209
+ for (const stage of this.stages.values()) if (stage.stepNumMap.has(parsed.stepNum)) {
3210
+ stage.cachedStepNums.add(parsed.stepNum);
3211
+ break;
3212
+ }
3213
+ break;
3214
+ case "done":
3215
+ for (const stage of this.stages.values()) if (stage.stepNumMap.has(parsed.stepNum)) {
3216
+ stage.doneStepNums.add(parsed.stepNum);
3217
+ break;
3218
+ }
3219
+ break;
3220
+ case "platform": {
3221
+ const cleaned = cleanDisplayLine(parsed.message);
3222
+ if (cleaned) this.platformMessages.push(cleaned);
3223
+ break;
3224
+ }
3225
+ case "output":
3226
+ case "other": break;
3227
+ }
3228
+ this.render();
3229
+ }
3230
+ printRuntimeLine(line) {
3231
+ const parsed = parseBuildLine(line);
3232
+ let text = null;
3233
+ if (parsed.kind === "platform") text = parsed.message;
3234
+ else if (parsed.kind === "other" && parsed.text) text = parsed.text;
3235
+ else if (parsed.kind === "output") text = parsed.text;
3236
+ if (!text) return;
3237
+ if (isHiddenMessage(text)) return;
3238
+ if (this.externalSpinner) this.externalSpinner.clear();
3239
+ if (!this.runtimeHeaderPrinted) {
3240
+ this.runtimeHeaderPrinted = true;
3241
+ console.log(chalk.cyan.bold(`\n RUNTIME`));
3242
+ }
3243
+ console.log(` ${text}`);
3244
+ if (this.externalSpinner) this.externalSpinner.render();
3245
+ }
3246
+ isStageComplete(stage) {
3247
+ return stage.steps.size >= stage.total;
3248
+ }
3249
+ render() {
3250
+ if (this.renderLineCount > 0) process.stdout.write(`\x1b[${this.renderLineCount}A\x1b[J`);
3251
+ let lines = 0;
3252
+ const label = this.serviceName ? `BUILD ${chalk.dim(`(${this.serviceName})`)}` : "BUILD";
3253
+ process.stdout.write(`${chalk.cyan.bold(` ${label}`)}\n`);
3254
+ lines++;
3255
+ for (const msg of this.platformMessages) {
3256
+ process.stdout.write(` ${msg}\n`);
3257
+ lines++;
3258
+ }
3259
+ if (this.stageOrder.length > 0) {
3260
+ process.stdout.write("\n");
3261
+ lines++;
3262
+ }
3263
+ const maxNameLen = Math.max(...this.stageOrder.map((n) => n.length), 4);
3264
+ for (let i = 0; i < this.stageOrder.length; i++) {
3265
+ const stageName = this.stageOrder[i];
3266
+ const stage = this.stages.get(stageName);
3267
+ const complete = this.isStageComplete(stage);
3268
+ const allCached = complete && stage.cachedStepNums.size === stage.steps.size;
3269
+ const allDone = complete && !allCached;
3270
+ const bar = renderProgressBar(stage.steps.size, stage.total, allCached, allDone);
3271
+ const paddedName = chalk.bold(stageName.padEnd(maxNameLen));
3272
+ process.stdout.write(` ${paddedName} ${bar}\n`);
3273
+ lines++;
3274
+ const sortedSteps = [...stage.steps.entries()].sort((a, b) => a[0] - b[0]);
3275
+ for (const [stepNum, command] of sortedSteps) {
3276
+ let stepStatus = "";
3277
+ for (const [bkNum, dockerStep] of stage.stepNumMap.entries()) if (dockerStep === stepNum) {
3278
+ if (stage.cachedStepNums.has(bkNum)) stepStatus = ` ${BRAND("◆")}`;
3279
+ else if (stage.doneStepNums.has(bkNum)) stepStatus = ` ${chalk.green("✓")}`;
3280
+ break;
3281
+ }
3282
+ process.stdout.write(` ${command}${stepStatus}\n`);
3283
+ lines++;
3284
+ }
3285
+ if (i < this.stageOrder.length - 1) {
3286
+ process.stdout.write("\n");
3287
+ lines++;
3288
+ }
3289
+ }
3290
+ if (this.spinnerInterval) {
3291
+ if (!(this.stageOrder.length > 0 && this.stageOrder.every((name) => this.isStageComplete(this.stages.get(name))))) {
3292
+ const frame = SPINNER_FRAMES[this.spinnerFrame];
3293
+ process.stdout.write(`\n ${chalk.cyan(frame)} ${this.spinnerText}\n`);
3294
+ lines += 2;
3295
+ }
3296
+ }
3297
+ this.renderLineCount = lines;
3298
+ }
3299
+ };
2963
3300
  async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
2964
3301
  const client = await getClient();
2965
3302
  const isVerbose = process.env.VELOZ_VERBOSE === "true";
@@ -2968,12 +3305,10 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
2968
3305
  const isGHA = !mcp && process.env.GITHUB_ACTIONS === "true";
2969
3306
  const allLogLines = [];
2970
3307
  let buildSpinner = null;
3308
+ let renderer = null;
2971
3309
  if (mcp) log(serviceName ? `[deploy] Build: ${serviceName}` : "[deploy] Build iniciando...");
2972
3310
  else if (isGHA) startGroup(serviceName ? `Build: ${serviceName}` : "Build");
2973
- else if (isTTY && !isVerbose) buildSpinner = ora({
2974
- text: "Aguardando início do build...",
2975
- color: "cyan"
2976
- }).start();
3311
+ else if (isTTY && !isVerbose) renderer = new BuildProgressRenderer(serviceName);
2977
3312
  else if (isTTY) {
2978
3313
  const header = serviceName ? `Build: ${chalk.bold(serviceName)}` : "Build";
2979
3314
  console.log(chalk.cyan(`\n${header}`));
@@ -2989,19 +3324,24 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
2989
3324
  const label = statusLabels[event.content] ?? event.content;
2990
3325
  finalStatus = event.content;
2991
3326
  if (mcp) log(`[deploy] Status: ${label}`);
2992
- else if (isTTY && !isVerbose) {
2993
- if (buildSpinner) {
2994
- if (event.content === "BUILDING") buildSpinner.text = "Construindo...";
2995
- else if (event.content === "DEPLOYING") {
2996
- buildSpinner.succeed("Build concluído");
2997
- buildSpinner = ora({
2998
- text: "Publicando...",
2999
- color: "cyan"
3000
- }).start();
3001
- } else if (event.content === "LIVE") {
3327
+ else if (renderer) {
3328
+ if (event.content === "BUILDING") renderer.setBuilding();
3329
+ else if (event.content === "DEPLOYING") {
3330
+ renderer.switchToRuntime();
3331
+ buildSpinner = ora({
3332
+ text: "Publicando...",
3333
+ color: "cyan"
3334
+ }).start();
3335
+ renderer.setExternalSpinner(buildSpinner);
3336
+ } else if (event.content === "LIVE") {
3337
+ if (buildSpinner) {
3338
+ renderer.setExternalSpinner(null);
3002
3339
  buildSpinner.succeed("Publicado");
3003
3340
  buildSpinner = null;
3004
- } else if (TERMINAL_STATUSES.has(event.content) && event.content !== "LIVE") {
3341
+ }
3342
+ } else if (TERMINAL_STATUSES.has(event.content) && event.content !== "LIVE") {
3343
+ if (buildSpinner) {
3344
+ renderer.setExternalSpinner(null);
3005
3345
  buildSpinner.fail(label);
3006
3346
  buildSpinner = null;
3007
3347
  }
@@ -3015,16 +3355,11 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
3015
3355
  allLogLines.push(...lines);
3016
3356
  if (mcp) {
3017
3357
  for (const line of lines) if (line.trim()) log(`[build] ${line.trim()}`);
3018
- } else if (isTTY && !isVerbose) for (const line of lines) {
3019
- const trimmed = line.trim();
3020
- if (trimmed) {
3021
- const display = trimmed.length > 60 ? trimmed.substring(0, 57) + "..." : trimmed;
3022
- if (buildSpinner) buildSpinner.text = display;
3023
- }
3024
- }
3358
+ } else if (renderer) for (const line of lines) renderer.processLine(line);
3025
3359
  else for (const line of lines) if (line.trim()) process.stdout.write(` ${line}\n`);
3026
3360
  }
3027
3361
  } catch {
3362
+ if (renderer) renderer.stopSpinner();
3028
3363
  if (buildSpinner) {
3029
3364
  buildSpinner.stop();
3030
3365
  buildSpinner = null;
@@ -3038,6 +3373,7 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
3038
3373
  } catch {}
3039
3374
  }
3040
3375
  if (isGHA) endGroup();
3376
+ if (renderer) renderer.stopSpinner();
3041
3377
  const urls = finalStatus === "LIVE" ? await fetchDeployUrls(client, serviceId) : [];
3042
3378
  if (finalStatus === "LIVE") {
3043
3379
  if (buildSpinner) {
@@ -3056,7 +3392,7 @@ async function streamDeploymentLogs(deploymentId, serviceId, serviceName) {
3056
3392
  if (mcp) {
3057
3393
  log(`✗ Deploy finalizou: ${label}`);
3058
3394
  for (const hint of hints) log(` → ${hint}`);
3059
- } else if (isTTY && allLogLines.length > 0) {
3395
+ } else if (isTTY && !renderer && allLogLines.length > 0) {
3060
3396
  console.log();
3061
3397
  console.log(chalk.red(` ${"─".repeat(50)}`));
3062
3398
  console.log(chalk.red.bold(" Logs de build:"));
@@ -3150,7 +3486,7 @@ const LOGO_LINES = [
3150
3486
  ];
3151
3487
  const BRAND_COLOR = "#FF4D00";
3152
3488
  function getVersion() {
3153
- return "0.0.0-beta.15";
3489
+ return "0.0.0-beta.16";
3154
3490
  }
3155
3491
  function printBanner(subtitle) {
3156
3492
  const version = getVersion();
@@ -3622,7 +3958,7 @@ async function autoUpdate() {
3622
3958
  if (process.env.VELOZ_MCP === "true") return;
3623
3959
  const pm = detectPackageManager();
3624
3960
  if (!pm) return;
3625
- const currentVersion = "0.0.0-beta.15";
3961
+ const currentVersion = "0.0.0-beta.16";
3626
3962
  const latestVersion = await fetchLatestVersion();
3627
3963
  if (!latestVersion || latestVersion === currentVersion) return;
3628
3964
  const installCmd = getInstallCommand(pm, latestVersion);
@@ -3698,7 +4034,13 @@ async function provisionDatabases(config, opts) {
3698
4034
  name: key,
3699
4035
  engine: dbConfig.engine,
3700
4036
  engineVersion: dbConfig.version,
3701
- storage: dbConfig.storage
4037
+ storage: dbConfig.storage,
4038
+ cpuLimit: dbConfig.resources?.cpu,
4039
+ memoryLimit: dbConfig.resources?.memory,
4040
+ poolerEnabled: dbConfig.pooler?.enabled,
4041
+ poolerPoolMode: dbConfig.pooler?.poolMode,
4042
+ poolerDefaultPoolSize: dbConfig.pooler?.defaultPoolSize,
4043
+ poolerMaxClientConn: dbConfig.pooler?.maxClientConn
3702
4044
  })
3703
4045
  });
3704
4046
  success(`Banco de dados "${key}" criado (provisionando...).`);
@@ -3733,9 +4075,13 @@ function getDatabaseUrlHints(config) {
3733
4075
  const databases = config.databases ?? {};
3734
4076
  const entries = Object.entries(databases);
3735
4077
  if (entries.length === 0) return [];
3736
- return entries.filter(([_, db]) => db.engine === "postgresql" || db.engine === "mysql").map(([key]) => {
3737
- return `${`${key.toUpperCase().replace(/-/g, "_")}_DATABASE_URL`} será injetado automaticamente de "${key}"`;
3738
- });
4078
+ const hints = [];
4079
+ for (const [key, db] of entries) if (db.engine === "postgresql" || db.engine === "mysql") {
4080
+ const prefix = key.toUpperCase().replace(/-/g, "_");
4081
+ hints.push(`${prefix}_DATABASE_URL será injetado automaticamente de "${key}"`);
4082
+ if (db.pooler?.enabled && db.engine === "postgresql") hints.push(`${prefix}_POOLER_URL será injetado automaticamente (PgBouncer)`);
4083
+ }
4084
+ return hints;
3739
4085
  }
3740
4086
 
3741
4087
  //#endregion
@@ -3789,21 +4135,18 @@ async function computeExtraFilesForServices(services) {
3789
4135
  return results;
3790
4136
  }
3791
4137
  async function triggerDeploy(serviceId, serviceName, preDetection) {
3792
- const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
3793
4138
  const sizeInBytes = await calculateDirectorySize(process.cwd());
3794
4139
  const sizeMB = Math.round(sizeInBytes / (1024 * 1024) * 10) / 10;
3795
- if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
3796
4140
  const client = await getClient();
3797
- const serviceConf = resolveServiceConf(loadConfig(), serviceId);
4141
+ const velozConfig = loadConfig();
4142
+ const serviceConf = resolveServiceConf(velozConfig, serviceId);
3798
4143
  const detection = preDetection ?? detectLocalRepo();
3799
4144
  warnIfEphemeralFsDetected(detection, serviceConf, serviceName ?? void 0);
3800
4145
  const extraFiles = prepareExtraFiles(detection, serviceConf);
3801
4146
  const warnings = runPreDeployChecks(serviceConf?.rootDirectory || ".");
3802
- if (warnings.length > 0) {
3803
- spinUpload.stop();
3804
- printDeployWarnings(warnings);
3805
- spinUpload.start();
3806
- }
4147
+ if (warnings.length > 0) printDeployWarnings(warnings);
4148
+ const spinUpload = spinner(serviceName ? `Fazendo upload ${chalk.bold(serviceName)}...` : "Fazendo upload do código...");
4149
+ if (sizeMB > 5) spinUpload.text = `Fazendo upload (${sizeMB} MB)...`;
3807
4150
  spinUpload.text = "Iniciando deploy...";
3808
4151
  const deployment = await withRetry(() => client.deployments.create({
3809
4152
  serviceId,
@@ -3816,7 +4159,12 @@ async function triggerDeploy(serviceId, serviceName, preDetection) {
3816
4159
  setupSigintHandler();
3817
4160
  trackDeployment(deployment.id);
3818
4161
  try {
3819
- return await streamDeploymentLogs(deployment.id, serviceId, serviceName);
4162
+ const result = await streamDeploymentLogs(deployment.id, serviceId, serviceName);
4163
+ if (result.status === "LIVE" && velozConfig?.project?.id) {
4164
+ const dashUrl = `${process.env.VELOZ_WEB_URL || "https://app.onveloz.com"}/projetos/${velozConfig.project.id}`;
4165
+ info(`Dashboard: ${chalk.dim(dashUrl)}`);
4166
+ }
4167
+ return result;
3820
4168
  } finally {
3821
4169
  untrackDeployment(deployment.id);
3822
4170
  }
@@ -3876,15 +4224,56 @@ async function maybeConfigurePersistentVolume(serviceConfig, detection, opts, se
3876
4224
  async function findServicesFromConfig() {
3877
4225
  const config = loadConfig();
3878
4226
  if (!config) return [];
4227
+ const missingIds = Object.entries(config.services).filter(([_, svc]) => !svc.id);
4228
+ if (missingIds.length > 0 && config.project.id) {
4229
+ const client = await getClient();
4230
+ const remoteServices = await withRetry(() => client.services.list({ projectId: config.project.id }));
4231
+ let configUpdated = false;
4232
+ for (const [key, serviceConfig] of missingIds) {
4233
+ const match = remoteServices.find((rs) => rs.name.toLowerCase() === serviceConfig.name.toLowerCase() || rs.name.toLowerCase() === key.toLowerCase());
4234
+ if (match) {
4235
+ serviceConfig.id = match.id;
4236
+ config.services[key] = serviceConfig;
4237
+ configUpdated = true;
4238
+ info(`Serviço "${serviceConfig.name}" vinculado (ID: ${match.id})`);
4239
+ } else {
4240
+ info(`Serviço "${serviceConfig.name}" não encontrado no projeto.`);
4241
+ if (await promptConfirm(`Criar serviço "${serviceConfig.name}" no projeto "${config.project.name}"?`)) {
4242
+ const branch = getGitBranch();
4243
+ const serviceType = serviceConfig.type?.toUpperCase() ?? "WEB";
4244
+ serviceConfig.id = (await withSpinner({
4245
+ text: `Criando serviço "${serviceConfig.name}"...`,
4246
+ fn: () => withRetry(() => client.services.create({
4247
+ projectId: config.project.id,
4248
+ name: serviceConfig.name,
4249
+ type: serviceType,
4250
+ branch,
4251
+ rootDirectory: serviceConfig.root ?? "."
4252
+ }))
4253
+ })).id;
4254
+ config.services[key] = serviceConfig;
4255
+ configUpdated = true;
4256
+ success(`Serviço "${serviceConfig.name}" criado.`);
4257
+ }
4258
+ }
4259
+ }
4260
+ if (configUpdated) {
4261
+ saveConfig(config);
4262
+ info(`Arquivo ${getConfigFileName()} atualizado com IDs dos serviços.`);
4263
+ }
4264
+ }
3879
4265
  const services = [];
3880
- for (const [key, serviceConfig] of Object.entries(config.services)) services.push({
3881
- path: resolve(process.cwd(), serviceConfig.root ?? "."),
3882
- serviceId: serviceConfig.id,
3883
- projectId: config.project.id,
3884
- serviceName: serviceConfig.name,
3885
- projectName: config.project.name,
3886
- key
3887
- });
4266
+ for (const [key, serviceConfig] of Object.entries(config.services)) {
4267
+ if (!serviceConfig.id) continue;
4268
+ services.push({
4269
+ path: resolve(process.cwd(), serviceConfig.root ?? "."),
4270
+ serviceId: serviceConfig.id,
4271
+ projectId: config.project.id,
4272
+ serviceName: serviceConfig.name,
4273
+ projectName: config.project.name,
4274
+ key
4275
+ });
4276
+ }
3888
4277
  return services;
3889
4278
  }
3890
4279
  function readLocalFile(path) {
@@ -4640,7 +5029,8 @@ async function cliDeployFlow(opts) {
4640
5029
  const available = configuredServices.map((s) => ` • ${s.key} (${s.serviceName})`).join("\n");
4641
5030
  throw new Error(`Serviço '${opts.service}' não encontrado.\n\nServiços disponíveis:\n${available}`);
4642
5031
  }
4643
- return await triggerDeploy(found.serviceId, found.serviceName);
5032
+ await triggerDeploy(found.serviceId, found.serviceName);
5033
+ return;
4644
5034
  }
4645
5035
  if (opts.all || opts.yes || configuredServices.length === 1) {
4646
5036
  if (configuredServices.length > 1 && !opts.yes) {
@@ -4655,25 +5045,25 @@ async function cliDeployFlow(opts) {
4655
5045
  return;
4656
5046
  }
4657
5047
  }
4658
- if (configuredServices.length === 1) return await triggerDeploy(configuredServices[0].serviceId, configuredServices[0].serviceName);
4659
- else return await deployServicesInParallel(await computeExtraFilesForServices(configuredServices));
4660
- } else {
4661
- console.log(chalk.bold("\nServiços disponíveis:\n"));
4662
- const selectedServiceIds = await promptMultiSelect("Quais serviços deseja fazer deploy?", configuredServices.map((s) => {
4663
- const relPath = relative(process.cwd(), s.path) || ".";
4664
- return {
4665
- label: `${s.serviceName} ${chalk.dim(`(${relPath})`)}`,
4666
- value: s.serviceId
4667
- };
4668
- }));
4669
- const selectedServices = configuredServices.filter((s) => selectedServiceIds.includes(s.serviceId));
4670
- if (selectedServices.length === 0) {
4671
- info("Nenhum serviço selecionado.");
4672
- return;
4673
- }
4674
- if (selectedServices.length === 1) return await triggerDeploy(selectedServices[0].serviceId, selectedServices[0].serviceName);
4675
- else return await deployServicesInParallel(await computeExtraFilesForServices(selectedServices));
5048
+ if (configuredServices.length === 1) await triggerDeploy(configuredServices[0].serviceId, configuredServices[0].serviceName);
5049
+ else await deployServicesInParallel(await computeExtraFilesForServices(configuredServices));
5050
+ return;
5051
+ }
5052
+ console.log(chalk.bold("\nServiços disponíveis:\n"));
5053
+ const selectedServiceIds = await promptMultiSelect("Quais serviços deseja fazer deploy?", configuredServices.map((s) => {
5054
+ const relPath = relative(process.cwd(), s.path) || ".";
5055
+ return {
5056
+ label: `${s.serviceName} ${chalk.dim(`(${relPath})`)}`,
5057
+ value: s.serviceId
5058
+ };
5059
+ }));
5060
+ const selectedServices = configuredServices.filter((s) => selectedServiceIds.includes(s.serviceId));
5061
+ if (selectedServices.length === 0) {
5062
+ info("Nenhum serviço selecionado.");
5063
+ return;
4676
5064
  }
5065
+ if (selectedServices.length === 1) await triggerDeploy(selectedServices[0].serviceId, selectedServices[0].serviceName);
5066
+ else await deployServicesInParallel(await computeExtraFilesForServices(selectedServices));
4677
5067
  }
4678
5068
  if (!isGitRepo()) throw new Error("Este diretório não é um repositório git. Inicialize com `git init` e adicione um remote.");
4679
5069
  info("Detectando repositório git...");
@@ -4728,7 +5118,8 @@ async function cliDeployFlow(opts) {
4728
5118
  info(`Arquivo ${getConfigFileName()} criado na raiz do projeto.`);
4729
5119
  const freshConfig = loadConfig();
4730
5120
  if (freshConfig) await provisionDatabases(freshConfig, { yes: opts.yes ?? false });
4731
- return await triggerDeploy(serviceId$1, void 0, detection);
5121
+ await triggerDeploy(serviceId$1, void 0, detection);
5122
+ return;
4732
5123
  }
4733
5124
  if (project && project.services.length === 0) {
4734
5125
  info(`Projeto encontrado: ${chalk.bold(project.name)}`);
@@ -4739,7 +5130,8 @@ async function cliDeployFlow(opts) {
4739
5130
  });
4740
5131
  const freshConfig = loadConfig();
4741
5132
  if (freshConfig) await provisionDatabases(freshConfig, { yes: opts.yes ?? false });
4742
- return await triggerDeploy(serviceId$1);
5133
+ await triggerDeploy(serviceId$1);
5134
+ return;
4743
5135
  }
4744
5136
  info("Projeto não encontrado. Vamos criar um novo.");
4745
5137
  const projectName = opts.yes ? remote.repo : await prompt(`Nome do projeto: ${chalk.dim(`(${remote.repo})`)}`) || remote.repo;
@@ -4758,7 +5150,7 @@ async function cliDeployFlow(opts) {
4758
5150
  });
4759
5151
  const newConfig = loadConfig();
4760
5152
  if (newConfig) await provisionDatabases(newConfig, { yes: opts.yes ?? false });
4761
- return await triggerDeploy(serviceId);
5153
+ await triggerDeploy(serviceId);
4762
5154
  }
4763
5155
 
4764
5156
  //#endregion
@@ -5072,7 +5464,7 @@ async function pruneRemovedEntries(config, existingConfig, services, databases)
5072
5464
  //#region src/index.ts
5073
5465
  if (process.argv.includes("--mcp")) process.env.VELOZ_MCP = "true";
5074
5466
  const cli = Cli.create("veloz", {
5075
- version: "0.0.0-beta.15",
5467
+ version: "0.0.0-beta.16",
5076
5468
  description: "CLI da plataforma Veloz — deploy rápido para o Brasil",
5077
5469
  env: z.object({ VELOZ_ENV: z.string().optional().describe("Ambiente alvo (ex: preview, staging)") })
5078
5470
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onveloz",
3
- "version": "0.0.0-beta.15",
3
+ "version": "0.0.0-beta.16",
4
4
  "description": "CLI da plataforma Veloz — deploy rápido para o Brasil",
5
5
  "keywords": [
6
6
  "brasil",