@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.
- package/dist/commands/app/dev.js +29 -4
- package/dist/commands/webhook/trigger.js +4 -4
- package/dist/lib/app-env.js +14 -2
- package/dist/lib/dev-runner.js +16 -17
- package/package.json +1 -1
package/dist/commands/app/dev.js
CHANGED
|
@@ -67,9 +67,7 @@ class AppDev extends Command {
|
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
await this.verifyLocalDevServer(port, server, api, registered);
|
|
70
|
-
this.
|
|
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 {
|
|
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, ".."),
|
|
110
|
+
const envPath = join(resolve(cfgPath, ".."), APP_DEV_ENV_FILE);
|
|
111
111
|
const env = readEnvFile(envPath);
|
|
112
112
|
return webhookSecretForEvent(event, env);
|
|
113
113
|
}
|
package/dist/lib/app-env.js
CHANGED
|
@@ -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
|
|
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 = { ...
|
|
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
|
};
|
package/dist/lib/dev-runner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync,
|
|
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,
|
|
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 [
|
|
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,
|