@odeva/cli 0.0.8 → 0.0.10

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.
@@ -67,9 +67,7 @@ class AppDev extends Command {
67
67
  }
68
68
  });
69
69
  await this.verifyLocalDevServer(port, server, api, registered);
70
- this.log(ui.dim("Starting Cloudflare tunnel..."));
71
- const tunnel = await startQuickTunnel(port);
72
- this.log(ui.ok(`Tunnel ready at ${ui.code(tunnel.url)}`));
70
+ const tunnel = await this.startReachableTunnel(port, server);
73
71
  registered = await this.registerDevWebhooks(api, loaded, tunnel.url);
74
72
  this.log("");
75
73
  this.log(`${ui.bold("Tunnel:")} ${tunnel.url}`);
@@ -78,7 +76,6 @@ class AppDev extends Command {
78
76
  this.log(` ${ui.dim("\u2192")} ${reg.config.topic.padEnd(28)} ${reg.fullUrl}`);
79
77
  }
80
78
  this.log("");
81
- await this.verifyDevServerReachable(tunnel, port, server, api, registered);
82
79
  const syncedUrls = await this.syncDevAppUrls(api, loaded, tunnel.url);
83
80
  this.printDevAppInfo(syncedUrls);
84
81
  await this.ensureDevAppInstalled(api, loaded, tunnel, port, server, registered);
@@ -244,6 +241,34 @@ class AppDev extends Command {
244
241
  throw err;
245
242
  }
246
243
  }
244
+ async startReachableTunnel(port, server) {
245
+ let lastError;
246
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
247
+ this.log(ui.dim(`Starting Cloudflare tunnel${attempt > 1 ? ` (attempt ${attempt}/3)` : ""}...`));
248
+ const tunnel = await startQuickTunnel(port);
249
+ this.log(ui.ok(`Tunnel ready at ${ui.code(tunnel.url)}`));
250
+ this.log(ui.dim("Waiting 20s for trycloudflare DNS to propagate before first probe..."));
251
+ await new Promise((resolve) => setTimeout(resolve, 2e4));
252
+ this.log(ui.dim("Verifying dev server and tunnel... (trycloudflare can take up to 60s to propagate)"));
253
+ try {
254
+ const result = await waitForDevServerReachable({
255
+ localPort: port,
256
+ tunnelUrl: tunnel.url
257
+ });
258
+ this.log(ui.ok(`Tunnel verified via ${ui.code(result.path)} (HTTP ${result.tunnelStatus})`));
259
+ return tunnel;
260
+ } catch (err) {
261
+ lastError = err;
262
+ await tunnel.stop();
263
+ if (attempt < 3) {
264
+ const message = err instanceof Error ? err.message : String(err);
265
+ this.log(ui.warn(`Tunnel ${attempt}/3 was not reachable: ${message}`));
266
+ }
267
+ }
268
+ }
269
+ await server.stop();
270
+ throw lastError instanceof Error ? lastError : new CliError(String(lastError));
271
+ }
247
272
  async verifyDevServerReachable(tunnel, port, server, api, registered) {
248
273
  this.log(ui.dim("Verifying dev server and tunnel... (trycloudflare can take up to 60s to propagate)"));
249
274
  try {
@@ -1,13 +1,13 @@
1
1
  import { Args, Command, Flags } from "@oclif/core";
2
2
  import { readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { join, resolve } from "node:path";
4
+ import { APP_DEV_ENV_FILE, readEnvFile } from "../../lib/app-env.js";
4
5
  import { loadAppConfig, findAppConfigPath } from "../../lib/config.js";
5
6
  import { buildFixture, signPayload } from "../../lib/webhook-fixtures.js";
6
7
  import { CliError } from "../../lib/errors.js";
7
8
  import { withErrorHandling } from "../../lib/run.js";
8
- import { readEnvFile, webhookSecretForEvent } from "../../lib/dev-runner.js";
9
+ import { webhookSecretForEvent } from "../../lib/dev-runner.js";
9
10
  import { ui } from "../../lib/ui.js";
10
- import { join } from "node:path";
11
11
  class WebhookTrigger extends Command {
12
12
  static description = "Fire a sample webhook payload at a local handler (uses the secret from .env.odeva.local if present)";
13
13
  static examples = [
@@ -107,7 +107,7 @@ class WebhookTrigger extends Command {
107
107
  secretFromEnvFile(event) {
108
108
  const cfgPath = findAppConfigPath();
109
109
  if (!cfgPath) return void 0;
110
- const envPath = join(resolve(cfgPath, ".."), ".env.odeva.local");
110
+ const envPath = join(resolve(cfgPath, ".."), APP_DEV_ENV_FILE);
111
111
  const env = readEnvFile(envPath);
112
112
  return webhookSecretForEvent(event, env);
113
113
  }
@@ -1,8 +1,17 @@
1
1
  import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  const APP_ENV_FILE = ".odeva.env";
4
+ const APP_ENV_LOCAL_FILE = ".odeva.env.local";
5
+ const APP_DEV_ENV_FILE = ".env.odeva.local";
4
6
  function loadAppEnv(appConfigPath) {
5
- const envPath = join(dirname(appConfigPath), APP_ENV_FILE);
7
+ const dir = dirname(appConfigPath);
8
+ return {
9
+ ...readEnvFile(join(dir, APP_DEV_ENV_FILE)),
10
+ ...readEnvFile(join(dir, APP_ENV_FILE)),
11
+ ...readEnvFile(join(dir, APP_ENV_LOCAL_FILE))
12
+ };
13
+ }
14
+ function readEnvFile(envPath) {
6
15
  if (!existsSync(envPath)) return {};
7
16
  const raw = readFileSync(envPath, "utf8");
8
17
  const result = {};
@@ -19,7 +28,7 @@ function loadAppEnv(appConfigPath) {
19
28
  }
20
29
  function saveAppEnv(appConfigPath, values) {
21
30
  const envPath = join(dirname(appConfigPath), APP_ENV_FILE);
22
- const merged = { ...loadAppEnv(appConfigPath), ...values };
31
+ const merged = { ...readEnvFile(envPath), ...values };
23
32
  const body = [
24
33
  "# Auto-managed by `odeva` \u2014 do not commit. Add `.odeva.env` to .gitignore.",
25
34
  ...Object.entries(merged).map(([k, v]) => `${k}=${v}`),
@@ -33,7 +42,10 @@ function saveAppEnv(appConfigPath, values) {
33
42
  return envPath;
34
43
  }
35
44
  export {
45
+ APP_DEV_ENV_FILE,
36
46
  APP_ENV_FILE,
47
+ APP_ENV_LOCAL_FILE,
37
48
  loadAppEnv,
49
+ readEnvFile,
38
50
  saveAppEnv
39
51
  };
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { APP_ENV_FILE } from "./app-env.js";
4
+ import { APP_DEV_ENV_FILE, APP_ENV_FILE, APP_ENV_LOCAL_FILE, readEnvFile } from "./app-env.js";
5
5
  import { CliError } from "./errors.js";
6
6
  async function registerWebhookSubscriptions(api, appName, tunnelUrl, subscriptions) {
7
7
  const created = [];
@@ -167,7 +167,8 @@ function devAppTunnelUrl(opts) {
167
167
  }
168
168
  function writeDevEnvFile(cwd, registered) {
169
169
  if (registered.length === 0) return null;
170
- const path = join(cwd, ".env.odeva.local");
170
+ const path = join(cwd, APP_DEV_ENV_FILE);
171
+ const preserved = Object.entries(readEnvFile(path)).filter(([key]) => !key.startsWith("ODEVA_WEBHOOK_SECRET"));
171
172
  const lines = [
172
173
  "# Auto-generated by `odeva app dev`. Do not commit.",
173
174
  "# Reload your dev server to pick up these values."
@@ -178,6 +179,13 @@ function writeDevEnvFile(cwd, registered) {
178
179
  lines.push(`# ${reg.config.topic} \u2192 ${reg.fullUrl}`);
179
180
  lines.push(`ODEVA_WEBHOOK_SECRET__${webhookSecretEnvKey(reg.config.topic)}=${reg.secret}`);
180
181
  }
182
+ if (preserved.length > 0) {
183
+ lines.push("");
184
+ lines.push("# Preserved local values.");
185
+ for (const [key, value] of preserved) {
186
+ lines.push(`${key}=${value}`);
187
+ }
188
+ }
181
189
  writeFileSync(path, lines.join("\n") + "\n", { mode: 384 });
182
190
  return path;
183
191
  }
@@ -196,7 +204,11 @@ function webhookSecretForEvent(event, env) {
196
204
  return env[`ODEVA_WEBHOOK_SECRET__${webhookSecretEnvKey(event)}`] ?? env["ODEVA_WEBHOOK_SECRET"];
197
205
  }
198
206
  function watchedDevInputPaths(appConfigPath) {
199
- return [appConfigPath, join(dirname(appConfigPath), APP_ENV_FILE)];
207
+ return [
208
+ appConfigPath,
209
+ join(dirname(appConfigPath), APP_ENV_FILE),
210
+ join(dirname(appConfigPath), APP_ENV_LOCAL_FILE)
211
+ ];
200
212
  }
201
213
  function joinUrl(base, path) {
202
214
  const trimmedBase = base.replace(/\/$/, "");
@@ -341,25 +353,12 @@ function preflightChecks(cwd) {
341
353
  }
342
354
  return { warnings };
343
355
  }
344
- function readEnvFile(path) {
345
- if (!existsSync(path)) return {};
346
- const out = {};
347
- for (const line of readFileSync(path, "utf8").split("\n")) {
348
- const trimmed = line.trim();
349
- if (!trimmed || trimmed.startsWith("#")) continue;
350
- const eq = trimmed.indexOf("=");
351
- if (eq === -1) continue;
352
- out[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
353
- }
354
- return out;
355
- }
356
356
  export {
357
357
  cleanupSubscriptions,
358
358
  devAppTunnelUrl,
359
359
  devInstallCallbackUrl,
360
360
  ensureDevAppInstalled,
361
361
  preflightChecks,
362
- readEnvFile,
363
362
  registerWebhookSubscriptions,
364
363
  registeredWebhookEnv,
365
364
  spawnDevServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odeva/cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Build apps on the Odeva booking platform — scaffold, develop, deploy.",
5
5
  "license": "MIT",
6
6
  "type": "module",