@whatalo/cli-kit 1.0.2 → 1.1.1
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/bundle/index.cjs +438 -0
- package/dist/bundle/index.d.cts +159 -0
- package/dist/bundle/index.d.ts +159 -0
- package/dist/bundle/index.mjs +398 -0
- package/dist/config/index.cjs +3 -1
- package/dist/config/index.d.cts +3 -1
- package/dist/config/index.d.ts +3 -1
- package/dist/config/index.mjs +3 -1
- package/dist/{env-file-KvUHlLtI.d.cts → env-file-Qh_6c5K2.d.cts} +4 -0
- package/dist/{env-file-KvUHlLtI.d.ts → env-file-Qh_6c5K2.d.ts} +4 -0
- package/dist/index.cjs +76 -14
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +76 -14
- package/dist/tunnel/index.cjs +79 -14
- package/dist/tunnel/index.d.cts +27 -6
- package/dist/tunnel/index.d.ts +27 -6
- package/dist/tunnel/index.mjs +79 -14
- package/package.json +6 -1
package/dist/index.cjs
CHANGED
|
@@ -674,7 +674,9 @@ var import_promises4 = require("fs/promises");
|
|
|
674
674
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
675
675
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
676
676
|
var import_node_https = __toESM(require("https"), 1);
|
|
677
|
-
var TUNNEL_START_TIMEOUT_MS =
|
|
677
|
+
var TUNNEL_START_TIMEOUT_MS = 4e4;
|
|
678
|
+
var MAX_TUNNEL_RETRIES = 5;
|
|
679
|
+
var TUNNEL_RETRY_DELAY_MS = 1e3;
|
|
678
680
|
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
679
681
|
function getBinDir() {
|
|
680
682
|
return import_node_path4.default.join(import_node_os2.default.homedir(), ".whatalo", "bin");
|
|
@@ -785,9 +787,7 @@ async function downloadCloudflared(targetPath) {
|
|
|
785
787
|
info(`cloudflared saved to ${targetPath}`);
|
|
786
788
|
return targetPath;
|
|
787
789
|
}
|
|
788
|
-
|
|
789
|
-
const { localPort, protocol = "http" } = options;
|
|
790
|
-
const binaryPath = await ensureCloudflared();
|
|
790
|
+
function spawnTunnelAttempt(binaryPath, localPort, protocol) {
|
|
791
791
|
return new Promise((resolve, reject) => {
|
|
792
792
|
const child = (0, import_node_child_process.spawn)(
|
|
793
793
|
binaryPath,
|
|
@@ -825,16 +825,7 @@ async function createTunnel(options) {
|
|
|
825
825
|
if (match && !urlFound) {
|
|
826
826
|
urlFound = true;
|
|
827
827
|
clearTimeout(timeout);
|
|
828
|
-
|
|
829
|
-
const kill = () => new Promise((res) => {
|
|
830
|
-
if (child.exitCode !== null) {
|
|
831
|
-
res();
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
child.once("exit", () => res());
|
|
835
|
-
child.kill("SIGTERM");
|
|
836
|
-
});
|
|
837
|
-
resolve({ url: tunnelUrl, process: child, kill });
|
|
828
|
+
resolve([match[0], child]);
|
|
838
829
|
}
|
|
839
830
|
}
|
|
840
831
|
});
|
|
@@ -861,6 +852,77 @@ async function createTunnel(options) {
|
|
|
861
852
|
});
|
|
862
853
|
});
|
|
863
854
|
}
|
|
855
|
+
function attachHealthMonitor(child, isStopped, reconnect, onReconnect) {
|
|
856
|
+
child.on("close", (code2, signal) => {
|
|
857
|
+
if (isStopped()) return;
|
|
858
|
+
warn(
|
|
859
|
+
`Tunnel process exited unexpectedly (code=${code2 ?? "null"}, signal=${signal ?? "none"}). Reconnecting...`
|
|
860
|
+
);
|
|
861
|
+
reconnect().then(([newUrl, newChild]) => {
|
|
862
|
+
info(`Tunnel reconnected (webhooks): ${newUrl}`);
|
|
863
|
+
attachHealthMonitor(newChild, isStopped, reconnect, onReconnect);
|
|
864
|
+
if (onReconnect) {
|
|
865
|
+
void Promise.resolve(onReconnect(newUrl));
|
|
866
|
+
}
|
|
867
|
+
}).catch((err) => {
|
|
868
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
869
|
+
warn(`Tunnel reconnect failed: ${message}`);
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
async function createTunnel(options) {
|
|
874
|
+
const {
|
|
875
|
+
localPort,
|
|
876
|
+
protocol = "http",
|
|
877
|
+
maxRetries = MAX_TUNNEL_RETRIES,
|
|
878
|
+
onReconnect
|
|
879
|
+
} = options;
|
|
880
|
+
const binaryPath = await ensureCloudflared();
|
|
881
|
+
const spawnNew = () => spawnTunnelAttempt(binaryPath, localPort, protocol);
|
|
882
|
+
let lastError;
|
|
883
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
884
|
+
try {
|
|
885
|
+
if (attempt > 1) {
|
|
886
|
+
info(`Tunnel attempt ${attempt}/${maxRetries}...`);
|
|
887
|
+
}
|
|
888
|
+
const [tunnelUrl, child] = await spawnNew();
|
|
889
|
+
let stopped = false;
|
|
890
|
+
let currentChild = child;
|
|
891
|
+
attachHealthMonitor(
|
|
892
|
+
child,
|
|
893
|
+
() => stopped,
|
|
894
|
+
async () => {
|
|
895
|
+
const result = await spawnNew();
|
|
896
|
+
currentChild = result[1];
|
|
897
|
+
return result;
|
|
898
|
+
},
|
|
899
|
+
onReconnect
|
|
900
|
+
);
|
|
901
|
+
const kill = () => new Promise((res) => {
|
|
902
|
+
stopped = true;
|
|
903
|
+
if (currentChild.exitCode !== null) {
|
|
904
|
+
res();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
currentChild.once("exit", () => res());
|
|
908
|
+
currentChild.kill("SIGTERM");
|
|
909
|
+
});
|
|
910
|
+
return { url: tunnelUrl, process: child, kill };
|
|
911
|
+
} catch (err) {
|
|
912
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
913
|
+
if (attempt < maxRetries) {
|
|
914
|
+
warn(
|
|
915
|
+
`Tunnel startup failed (attempt ${attempt}/${maxRetries}): ${lastError.message}`
|
|
916
|
+
);
|
|
917
|
+
await new Promise((r) => setTimeout(r, TUNNEL_RETRY_DELAY_MS));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
throw new Error(
|
|
922
|
+
`Tunnel failed after ${maxRetries} attempts. Check your network or use \`--tunnel-url\` to provide a URL manually.
|
|
923
|
+
Last error: ${lastError?.message ?? "unknown"}`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
864
926
|
|
|
865
927
|
// src/version/check.ts
|
|
866
928
|
var import_promises5 = require("fs/promises");
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ export { clearSession, getSession, getSessionDir, isSessionValid, pollForToken,
|
|
|
2
2
|
export { D as DeviceCodeResponse, b as POLL_STATUS, P as PollResult, a as PollStatus, T as TokenResponse, W as WhataloSession } from './types-DunvRQ0f.cjs';
|
|
3
3
|
export { Spinner, TaskStatus, WhataloAuthError, WhataloConfigError, WhataloNetworkError, WhataloValidationError, banner, code, createSpinner, error, failMissingNonTTYFlags, handleCliError, info, link, renderInfoPanel, renderTable, renderTasks, success, table, warn, withErrorHandler } from './output/index.cjs';
|
|
4
4
|
export { HttpClientOptions, WhataloApiClient } from './http/index.cjs';
|
|
5
|
-
export { C as CONFIG_FILE_NAME, E as EnvEntry, W as WhataloAppConfig, p as parseEnvFile, r as readConfig, u as updateEnvFile, w as writeConfig } from './env-file-
|
|
5
|
+
export { C as CONFIG_FILE_NAME, E as EnvEntry, W as WhataloAppConfig, p as parseEnvFile, r as readConfig, u as updateEnvFile, w as writeConfig } from './env-file-Qh_6c5K2.cjs';
|
|
6
6
|
export { TunnelDownloadProgress, TunnelOptions, TunnelProcess, createTunnel, ensureCloudflared } from './tunnel/index.cjs';
|
|
7
7
|
export { VersionCheckResult, checkSdkCompatibility, getUpgradeCommand, isNewerVersion, scheduleVersionCheck } from './version/index.cjs';
|
|
8
8
|
import 'node:child_process';
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { clearSession, getSession, getSessionDir, isSessionValid, pollForToken,
|
|
|
2
2
|
export { D as DeviceCodeResponse, b as POLL_STATUS, P as PollResult, a as PollStatus, T as TokenResponse, W as WhataloSession } from './types-DunvRQ0f.js';
|
|
3
3
|
export { Spinner, TaskStatus, WhataloAuthError, WhataloConfigError, WhataloNetworkError, WhataloValidationError, banner, code, createSpinner, error, failMissingNonTTYFlags, handleCliError, info, link, renderInfoPanel, renderTable, renderTasks, success, table, warn, withErrorHandler } from './output/index.js';
|
|
4
4
|
export { HttpClientOptions, WhataloApiClient } from './http/index.js';
|
|
5
|
-
export { C as CONFIG_FILE_NAME, E as EnvEntry, W as WhataloAppConfig, p as parseEnvFile, r as readConfig, u as updateEnvFile, w as writeConfig } from './env-file-
|
|
5
|
+
export { C as CONFIG_FILE_NAME, E as EnvEntry, W as WhataloAppConfig, p as parseEnvFile, r as readConfig, u as updateEnvFile, w as writeConfig } from './env-file-Qh_6c5K2.js';
|
|
6
6
|
export { TunnelDownloadProgress, TunnelOptions, TunnelProcess, createTunnel, ensureCloudflared } from './tunnel/index.js';
|
|
7
7
|
export { VersionCheckResult, checkSdkCompatibility, getUpgradeCommand, isNewerVersion, scheduleVersionCheck } from './version/index.js';
|
|
8
8
|
import 'node:child_process';
|
package/dist/index.mjs
CHANGED
|
@@ -598,7 +598,9 @@ import { unlink, rename } from "fs/promises";
|
|
|
598
598
|
import path3 from "path";
|
|
599
599
|
import os2 from "os";
|
|
600
600
|
import https from "https";
|
|
601
|
-
var TUNNEL_START_TIMEOUT_MS =
|
|
601
|
+
var TUNNEL_START_TIMEOUT_MS = 4e4;
|
|
602
|
+
var MAX_TUNNEL_RETRIES = 5;
|
|
603
|
+
var TUNNEL_RETRY_DELAY_MS = 1e3;
|
|
602
604
|
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
603
605
|
function getBinDir() {
|
|
604
606
|
return path3.join(os2.homedir(), ".whatalo", "bin");
|
|
@@ -709,9 +711,7 @@ async function downloadCloudflared(targetPath) {
|
|
|
709
711
|
info(`cloudflared saved to ${targetPath}`);
|
|
710
712
|
return targetPath;
|
|
711
713
|
}
|
|
712
|
-
|
|
713
|
-
const { localPort, protocol = "http" } = options;
|
|
714
|
-
const binaryPath = await ensureCloudflared();
|
|
714
|
+
function spawnTunnelAttempt(binaryPath, localPort, protocol) {
|
|
715
715
|
return new Promise((resolve, reject) => {
|
|
716
716
|
const child = spawn(
|
|
717
717
|
binaryPath,
|
|
@@ -749,16 +749,7 @@ async function createTunnel(options) {
|
|
|
749
749
|
if (match && !urlFound) {
|
|
750
750
|
urlFound = true;
|
|
751
751
|
clearTimeout(timeout);
|
|
752
|
-
|
|
753
|
-
const kill = () => new Promise((res) => {
|
|
754
|
-
if (child.exitCode !== null) {
|
|
755
|
-
res();
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
child.once("exit", () => res());
|
|
759
|
-
child.kill("SIGTERM");
|
|
760
|
-
});
|
|
761
|
-
resolve({ url: tunnelUrl, process: child, kill });
|
|
752
|
+
resolve([match[0], child]);
|
|
762
753
|
}
|
|
763
754
|
}
|
|
764
755
|
});
|
|
@@ -785,6 +776,77 @@ async function createTunnel(options) {
|
|
|
785
776
|
});
|
|
786
777
|
});
|
|
787
778
|
}
|
|
779
|
+
function attachHealthMonitor(child, isStopped, reconnect, onReconnect) {
|
|
780
|
+
child.on("close", (code2, signal) => {
|
|
781
|
+
if (isStopped()) return;
|
|
782
|
+
warn(
|
|
783
|
+
`Tunnel process exited unexpectedly (code=${code2 ?? "null"}, signal=${signal ?? "none"}). Reconnecting...`
|
|
784
|
+
);
|
|
785
|
+
reconnect().then(([newUrl, newChild]) => {
|
|
786
|
+
info(`Tunnel reconnected (webhooks): ${newUrl}`);
|
|
787
|
+
attachHealthMonitor(newChild, isStopped, reconnect, onReconnect);
|
|
788
|
+
if (onReconnect) {
|
|
789
|
+
void Promise.resolve(onReconnect(newUrl));
|
|
790
|
+
}
|
|
791
|
+
}).catch((err) => {
|
|
792
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
793
|
+
warn(`Tunnel reconnect failed: ${message}`);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
async function createTunnel(options) {
|
|
798
|
+
const {
|
|
799
|
+
localPort,
|
|
800
|
+
protocol = "http",
|
|
801
|
+
maxRetries = MAX_TUNNEL_RETRIES,
|
|
802
|
+
onReconnect
|
|
803
|
+
} = options;
|
|
804
|
+
const binaryPath = await ensureCloudflared();
|
|
805
|
+
const spawnNew = () => spawnTunnelAttempt(binaryPath, localPort, protocol);
|
|
806
|
+
let lastError;
|
|
807
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
808
|
+
try {
|
|
809
|
+
if (attempt > 1) {
|
|
810
|
+
info(`Tunnel attempt ${attempt}/${maxRetries}...`);
|
|
811
|
+
}
|
|
812
|
+
const [tunnelUrl, child] = await spawnNew();
|
|
813
|
+
let stopped = false;
|
|
814
|
+
let currentChild = child;
|
|
815
|
+
attachHealthMonitor(
|
|
816
|
+
child,
|
|
817
|
+
() => stopped,
|
|
818
|
+
async () => {
|
|
819
|
+
const result = await spawnNew();
|
|
820
|
+
currentChild = result[1];
|
|
821
|
+
return result;
|
|
822
|
+
},
|
|
823
|
+
onReconnect
|
|
824
|
+
);
|
|
825
|
+
const kill = () => new Promise((res) => {
|
|
826
|
+
stopped = true;
|
|
827
|
+
if (currentChild.exitCode !== null) {
|
|
828
|
+
res();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
currentChild.once("exit", () => res());
|
|
832
|
+
currentChild.kill("SIGTERM");
|
|
833
|
+
});
|
|
834
|
+
return { url: tunnelUrl, process: child, kill };
|
|
835
|
+
} catch (err) {
|
|
836
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
837
|
+
if (attempt < maxRetries) {
|
|
838
|
+
warn(
|
|
839
|
+
`Tunnel startup failed (attempt ${attempt}/${maxRetries}): ${lastError.message}`
|
|
840
|
+
);
|
|
841
|
+
await new Promise((r) => setTimeout(r, TUNNEL_RETRY_DELAY_MS));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
throw new Error(
|
|
846
|
+
`Tunnel failed after ${maxRetries} attempts. Check your network or use \`--tunnel-url\` to provide a URL manually.
|
|
847
|
+
Last error: ${lastError?.message ?? "unknown"}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
788
850
|
|
|
789
851
|
// src/version/check.ts
|
|
790
852
|
import { readFile as readFile3, writeFile as writeFile3, mkdir } from "fs/promises";
|
package/dist/tunnel/index.cjs
CHANGED
|
@@ -45,6 +45,9 @@ var import_node_https = __toESM(require("https"), 1);
|
|
|
45
45
|
|
|
46
46
|
// src/output/format.ts
|
|
47
47
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
48
|
+
function warn(message) {
|
|
49
|
+
console.log(` ${import_chalk.default.yellow("\u26A0")} ${message}`);
|
|
50
|
+
}
|
|
48
51
|
function info(message) {
|
|
49
52
|
console.log(` ${import_chalk.default.blue("\u2139")} ${message}`);
|
|
50
53
|
}
|
|
@@ -58,7 +61,9 @@ var STATUS_ICONS = {
|
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
// src/tunnel/cloudflared.ts
|
|
61
|
-
var TUNNEL_START_TIMEOUT_MS =
|
|
64
|
+
var TUNNEL_START_TIMEOUT_MS = 4e4;
|
|
65
|
+
var MAX_TUNNEL_RETRIES = 5;
|
|
66
|
+
var TUNNEL_RETRY_DELAY_MS = 1e3;
|
|
62
67
|
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
63
68
|
function getBinDir() {
|
|
64
69
|
return import_node_path.default.join(import_node_os.default.homedir(), ".whatalo", "bin");
|
|
@@ -169,9 +174,7 @@ async function downloadCloudflared(targetPath) {
|
|
|
169
174
|
info(`cloudflared saved to ${targetPath}`);
|
|
170
175
|
return targetPath;
|
|
171
176
|
}
|
|
172
|
-
|
|
173
|
-
const { localPort, protocol = "http" } = options;
|
|
174
|
-
const binaryPath = await ensureCloudflared();
|
|
177
|
+
function spawnTunnelAttempt(binaryPath, localPort, protocol) {
|
|
175
178
|
return new Promise((resolve, reject) => {
|
|
176
179
|
const child = (0, import_node_child_process.spawn)(
|
|
177
180
|
binaryPath,
|
|
@@ -209,16 +212,7 @@ async function createTunnel(options) {
|
|
|
209
212
|
if (match && !urlFound) {
|
|
210
213
|
urlFound = true;
|
|
211
214
|
clearTimeout(timeout);
|
|
212
|
-
|
|
213
|
-
const kill = () => new Promise((res) => {
|
|
214
|
-
if (child.exitCode !== null) {
|
|
215
|
-
res();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
child.once("exit", () => res());
|
|
219
|
-
child.kill("SIGTERM");
|
|
220
|
-
});
|
|
221
|
-
resolve({ url: tunnelUrl, process: child, kill });
|
|
215
|
+
resolve([match[0], child]);
|
|
222
216
|
}
|
|
223
217
|
}
|
|
224
218
|
});
|
|
@@ -245,6 +239,77 @@ async function createTunnel(options) {
|
|
|
245
239
|
});
|
|
246
240
|
});
|
|
247
241
|
}
|
|
242
|
+
function attachHealthMonitor(child, isStopped, reconnect, onReconnect) {
|
|
243
|
+
child.on("close", (code, signal) => {
|
|
244
|
+
if (isStopped()) return;
|
|
245
|
+
warn(
|
|
246
|
+
`Tunnel process exited unexpectedly (code=${code ?? "null"}, signal=${signal ?? "none"}). Reconnecting...`
|
|
247
|
+
);
|
|
248
|
+
reconnect().then(([newUrl, newChild]) => {
|
|
249
|
+
info(`Tunnel reconnected (webhooks): ${newUrl}`);
|
|
250
|
+
attachHealthMonitor(newChild, isStopped, reconnect, onReconnect);
|
|
251
|
+
if (onReconnect) {
|
|
252
|
+
void Promise.resolve(onReconnect(newUrl));
|
|
253
|
+
}
|
|
254
|
+
}).catch((err) => {
|
|
255
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
256
|
+
warn(`Tunnel reconnect failed: ${message}`);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async function createTunnel(options) {
|
|
261
|
+
const {
|
|
262
|
+
localPort,
|
|
263
|
+
protocol = "http",
|
|
264
|
+
maxRetries = MAX_TUNNEL_RETRIES,
|
|
265
|
+
onReconnect
|
|
266
|
+
} = options;
|
|
267
|
+
const binaryPath = await ensureCloudflared();
|
|
268
|
+
const spawnNew = () => spawnTunnelAttempt(binaryPath, localPort, protocol);
|
|
269
|
+
let lastError;
|
|
270
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
271
|
+
try {
|
|
272
|
+
if (attempt > 1) {
|
|
273
|
+
info(`Tunnel attempt ${attempt}/${maxRetries}...`);
|
|
274
|
+
}
|
|
275
|
+
const [tunnelUrl, child] = await spawnNew();
|
|
276
|
+
let stopped = false;
|
|
277
|
+
let currentChild = child;
|
|
278
|
+
attachHealthMonitor(
|
|
279
|
+
child,
|
|
280
|
+
() => stopped,
|
|
281
|
+
async () => {
|
|
282
|
+
const result = await spawnNew();
|
|
283
|
+
currentChild = result[1];
|
|
284
|
+
return result;
|
|
285
|
+
},
|
|
286
|
+
onReconnect
|
|
287
|
+
);
|
|
288
|
+
const kill = () => new Promise((res) => {
|
|
289
|
+
stopped = true;
|
|
290
|
+
if (currentChild.exitCode !== null) {
|
|
291
|
+
res();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
currentChild.once("exit", () => res());
|
|
295
|
+
currentChild.kill("SIGTERM");
|
|
296
|
+
});
|
|
297
|
+
return { url: tunnelUrl, process: child, kill };
|
|
298
|
+
} catch (err) {
|
|
299
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
300
|
+
if (attempt < maxRetries) {
|
|
301
|
+
warn(
|
|
302
|
+
`Tunnel startup failed (attempt ${attempt}/${maxRetries}): ${lastError.message}`
|
|
303
|
+
);
|
|
304
|
+
await new Promise((r) => setTimeout(r, TUNNEL_RETRY_DELAY_MS));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Tunnel failed after ${maxRetries} attempts. Check your network or use \`--tunnel-url\` to provide a URL manually.
|
|
310
|
+
Last error: ${lastError?.message ?? "unknown"}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
248
313
|
// Annotate the CommonJS export names for ESM import in node:
|
|
249
314
|
0 && (module.exports = {
|
|
250
315
|
createTunnel,
|
package/dist/tunnel/index.d.cts
CHANGED
|
@@ -6,6 +6,17 @@ interface TunnelOptions {
|
|
|
6
6
|
localPort: number;
|
|
7
7
|
/** Protocol to use for the tunnel connection (default: "http") */
|
|
8
8
|
protocol?: "http" | "https";
|
|
9
|
+
/**
|
|
10
|
+
* Maximum number of times to retry tunnel creation on failure.
|
|
11
|
+
* Defaults to MAX_TUNNEL_RETRIES (5).
|
|
12
|
+
*/
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Called whenever the tunnel reconnects after an unexpected process exit.
|
|
16
|
+
* Receives the new public URL so the caller can update the dev session.
|
|
17
|
+
* Optional — callers that do not need URL update notifications can omit this.
|
|
18
|
+
*/
|
|
19
|
+
onReconnect?: (newUrl: string) => void | Promise<void>;
|
|
9
20
|
}
|
|
10
21
|
/**
|
|
11
22
|
* Represents a running tunnel process.
|
|
@@ -37,7 +48,7 @@ interface TunnelDownloadProgress {
|
|
|
37
48
|
* Binary resolution order:
|
|
38
49
|
* 1. System PATH (user's global install, resolved via `which`/`where`)
|
|
39
50
|
* 2. ~/.whatalo/bin/cloudflared (previously downloaded by the CLI)
|
|
40
|
-
* 3. Download from GitHub releases
|
|
51
|
+
* 3. Download from GitHub releases -> ~/.whatalo/bin/cloudflared
|
|
41
52
|
*
|
|
42
53
|
* Note: process-spawning helpers use spawnSync / spawn (not exec/execSync)
|
|
43
54
|
* to avoid shell interpolation entirely — all arguments are passed as an
|
|
@@ -56,14 +67,24 @@ interface TunnelDownloadProgress {
|
|
|
56
67
|
*/
|
|
57
68
|
declare function ensureCloudflared(): Promise<string>;
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
70
|
+
* Creates a cloudflared quick tunnel targeting the given local port.
|
|
60
71
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
72
|
+
* Enhancements over a bare spawn:
|
|
73
|
+
*
|
|
74
|
+
* - 40-second startup timeout (previously 15 s) to handle slow initialization
|
|
75
|
+
* - Up to MAX_TUNNEL_RETRIES (5) automatic retry attempts with a 1-second
|
|
76
|
+
* delay between each before giving up
|
|
77
|
+
* - Health monitoring via attachHealthMonitor: detects unexpected process exit
|
|
78
|
+
* and auto-reconnects; calls options.onReconnect with the new URL so the
|
|
79
|
+
* caller can patch server-side state (e.g., dev session tunnel URL)
|
|
80
|
+
*
|
|
81
|
+
* The tunnel is primarily used for webhook and API callbacks. Plugin iframes
|
|
82
|
+
* are served directly from the developer's local server via the CDN dev bundle,
|
|
83
|
+
* so tunnel availability affects event delivery, not page rendering.
|
|
63
84
|
*
|
|
64
85
|
* @param options - localPort is required; protocol defaults to "http"
|
|
65
|
-
* @returns A TunnelProcess
|
|
66
|
-
* @throws
|
|
86
|
+
* @returns A TunnelProcess with the current public URL and a kill() helper
|
|
87
|
+
* @throws After exhausting all retries with a descriptive aggregated error
|
|
67
88
|
*/
|
|
68
89
|
declare function createTunnel(options: TunnelOptions): Promise<TunnelProcess>;
|
|
69
90
|
|
package/dist/tunnel/index.d.ts
CHANGED
|
@@ -6,6 +6,17 @@ interface TunnelOptions {
|
|
|
6
6
|
localPort: number;
|
|
7
7
|
/** Protocol to use for the tunnel connection (default: "http") */
|
|
8
8
|
protocol?: "http" | "https";
|
|
9
|
+
/**
|
|
10
|
+
* Maximum number of times to retry tunnel creation on failure.
|
|
11
|
+
* Defaults to MAX_TUNNEL_RETRIES (5).
|
|
12
|
+
*/
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Called whenever the tunnel reconnects after an unexpected process exit.
|
|
16
|
+
* Receives the new public URL so the caller can update the dev session.
|
|
17
|
+
* Optional — callers that do not need URL update notifications can omit this.
|
|
18
|
+
*/
|
|
19
|
+
onReconnect?: (newUrl: string) => void | Promise<void>;
|
|
9
20
|
}
|
|
10
21
|
/**
|
|
11
22
|
* Represents a running tunnel process.
|
|
@@ -37,7 +48,7 @@ interface TunnelDownloadProgress {
|
|
|
37
48
|
* Binary resolution order:
|
|
38
49
|
* 1. System PATH (user's global install, resolved via `which`/`where`)
|
|
39
50
|
* 2. ~/.whatalo/bin/cloudflared (previously downloaded by the CLI)
|
|
40
|
-
* 3. Download from GitHub releases
|
|
51
|
+
* 3. Download from GitHub releases -> ~/.whatalo/bin/cloudflared
|
|
41
52
|
*
|
|
42
53
|
* Note: process-spawning helpers use spawnSync / spawn (not exec/execSync)
|
|
43
54
|
* to avoid shell interpolation entirely — all arguments are passed as an
|
|
@@ -56,14 +67,24 @@ interface TunnelDownloadProgress {
|
|
|
56
67
|
*/
|
|
57
68
|
declare function ensureCloudflared(): Promise<string>;
|
|
58
69
|
/**
|
|
59
|
-
*
|
|
70
|
+
* Creates a cloudflared quick tunnel targeting the given local port.
|
|
60
71
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
72
|
+
* Enhancements over a bare spawn:
|
|
73
|
+
*
|
|
74
|
+
* - 40-second startup timeout (previously 15 s) to handle slow initialization
|
|
75
|
+
* - Up to MAX_TUNNEL_RETRIES (5) automatic retry attempts with a 1-second
|
|
76
|
+
* delay between each before giving up
|
|
77
|
+
* - Health monitoring via attachHealthMonitor: detects unexpected process exit
|
|
78
|
+
* and auto-reconnects; calls options.onReconnect with the new URL so the
|
|
79
|
+
* caller can patch server-side state (e.g., dev session tunnel URL)
|
|
80
|
+
*
|
|
81
|
+
* The tunnel is primarily used for webhook and API callbacks. Plugin iframes
|
|
82
|
+
* are served directly from the developer's local server via the CDN dev bundle,
|
|
83
|
+
* so tunnel availability affects event delivery, not page rendering.
|
|
63
84
|
*
|
|
64
85
|
* @param options - localPort is required; protocol defaults to "http"
|
|
65
|
-
* @returns A TunnelProcess
|
|
66
|
-
* @throws
|
|
86
|
+
* @returns A TunnelProcess with the current public URL and a kill() helper
|
|
87
|
+
* @throws After exhausting all retries with a descriptive aggregated error
|
|
67
88
|
*/
|
|
68
89
|
declare function createTunnel(options: TunnelOptions): Promise<TunnelProcess>;
|
|
69
90
|
|
package/dist/tunnel/index.mjs
CHANGED
|
@@ -8,6 +8,9 @@ import https from "https";
|
|
|
8
8
|
|
|
9
9
|
// src/output/format.ts
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
+
function warn(message) {
|
|
12
|
+
console.log(` ${chalk.yellow("\u26A0")} ${message}`);
|
|
13
|
+
}
|
|
11
14
|
function info(message) {
|
|
12
15
|
console.log(` ${chalk.blue("\u2139")} ${message}`);
|
|
13
16
|
}
|
|
@@ -21,7 +24,9 @@ var STATUS_ICONS = {
|
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
// src/tunnel/cloudflared.ts
|
|
24
|
-
var TUNNEL_START_TIMEOUT_MS =
|
|
27
|
+
var TUNNEL_START_TIMEOUT_MS = 4e4;
|
|
28
|
+
var MAX_TUNNEL_RETRIES = 5;
|
|
29
|
+
var TUNNEL_RETRY_DELAY_MS = 1e3;
|
|
25
30
|
var TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
26
31
|
function getBinDir() {
|
|
27
32
|
return path.join(os.homedir(), ".whatalo", "bin");
|
|
@@ -132,9 +137,7 @@ async function downloadCloudflared(targetPath) {
|
|
|
132
137
|
info(`cloudflared saved to ${targetPath}`);
|
|
133
138
|
return targetPath;
|
|
134
139
|
}
|
|
135
|
-
|
|
136
|
-
const { localPort, protocol = "http" } = options;
|
|
137
|
-
const binaryPath = await ensureCloudflared();
|
|
140
|
+
function spawnTunnelAttempt(binaryPath, localPort, protocol) {
|
|
138
141
|
return new Promise((resolve, reject) => {
|
|
139
142
|
const child = spawn(
|
|
140
143
|
binaryPath,
|
|
@@ -172,16 +175,7 @@ async function createTunnel(options) {
|
|
|
172
175
|
if (match && !urlFound) {
|
|
173
176
|
urlFound = true;
|
|
174
177
|
clearTimeout(timeout);
|
|
175
|
-
|
|
176
|
-
const kill = () => new Promise((res) => {
|
|
177
|
-
if (child.exitCode !== null) {
|
|
178
|
-
res();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
child.once("exit", () => res());
|
|
182
|
-
child.kill("SIGTERM");
|
|
183
|
-
});
|
|
184
|
-
resolve({ url: tunnelUrl, process: child, kill });
|
|
178
|
+
resolve([match[0], child]);
|
|
185
179
|
}
|
|
186
180
|
}
|
|
187
181
|
});
|
|
@@ -208,6 +202,77 @@ async function createTunnel(options) {
|
|
|
208
202
|
});
|
|
209
203
|
});
|
|
210
204
|
}
|
|
205
|
+
function attachHealthMonitor(child, isStopped, reconnect, onReconnect) {
|
|
206
|
+
child.on("close", (code, signal) => {
|
|
207
|
+
if (isStopped()) return;
|
|
208
|
+
warn(
|
|
209
|
+
`Tunnel process exited unexpectedly (code=${code ?? "null"}, signal=${signal ?? "none"}). Reconnecting...`
|
|
210
|
+
);
|
|
211
|
+
reconnect().then(([newUrl, newChild]) => {
|
|
212
|
+
info(`Tunnel reconnected (webhooks): ${newUrl}`);
|
|
213
|
+
attachHealthMonitor(newChild, isStopped, reconnect, onReconnect);
|
|
214
|
+
if (onReconnect) {
|
|
215
|
+
void Promise.resolve(onReconnect(newUrl));
|
|
216
|
+
}
|
|
217
|
+
}).catch((err) => {
|
|
218
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
219
|
+
warn(`Tunnel reconnect failed: ${message}`);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function createTunnel(options) {
|
|
224
|
+
const {
|
|
225
|
+
localPort,
|
|
226
|
+
protocol = "http",
|
|
227
|
+
maxRetries = MAX_TUNNEL_RETRIES,
|
|
228
|
+
onReconnect
|
|
229
|
+
} = options;
|
|
230
|
+
const binaryPath = await ensureCloudflared();
|
|
231
|
+
const spawnNew = () => spawnTunnelAttempt(binaryPath, localPort, protocol);
|
|
232
|
+
let lastError;
|
|
233
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
234
|
+
try {
|
|
235
|
+
if (attempt > 1) {
|
|
236
|
+
info(`Tunnel attempt ${attempt}/${maxRetries}...`);
|
|
237
|
+
}
|
|
238
|
+
const [tunnelUrl, child] = await spawnNew();
|
|
239
|
+
let stopped = false;
|
|
240
|
+
let currentChild = child;
|
|
241
|
+
attachHealthMonitor(
|
|
242
|
+
child,
|
|
243
|
+
() => stopped,
|
|
244
|
+
async () => {
|
|
245
|
+
const result = await spawnNew();
|
|
246
|
+
currentChild = result[1];
|
|
247
|
+
return result;
|
|
248
|
+
},
|
|
249
|
+
onReconnect
|
|
250
|
+
);
|
|
251
|
+
const kill = () => new Promise((res) => {
|
|
252
|
+
stopped = true;
|
|
253
|
+
if (currentChild.exitCode !== null) {
|
|
254
|
+
res();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
currentChild.once("exit", () => res());
|
|
258
|
+
currentChild.kill("SIGTERM");
|
|
259
|
+
});
|
|
260
|
+
return { url: tunnelUrl, process: child, kill };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
263
|
+
if (attempt < maxRetries) {
|
|
264
|
+
warn(
|
|
265
|
+
`Tunnel startup failed (attempt ${attempt}/${maxRetries}): ${lastError.message}`
|
|
266
|
+
);
|
|
267
|
+
await new Promise((r) => setTimeout(r, TUNNEL_RETRY_DELAY_MS));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Tunnel failed after ${maxRetries} attempts. Check your network or use \`--tunnel-url\` to provide a URL manually.
|
|
273
|
+
Last error: ${lastError?.message ?? "unknown"}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
211
276
|
export {
|
|
212
277
|
createTunnel,
|
|
213
278
|
ensureCloudflared
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whatalo/cli-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Shared CLI utilities for Whatalo plugin development tools",
|
|
@@ -64,6 +64,11 @@
|
|
|
64
64
|
"types": "./dist/version/index.d.ts",
|
|
65
65
|
"import": "./dist/version/index.mjs",
|
|
66
66
|
"require": "./dist/version/index.cjs"
|
|
67
|
+
},
|
|
68
|
+
"./bundle": {
|
|
69
|
+
"types": "./dist/bundle/index.d.ts",
|
|
70
|
+
"import": "./dist/bundle/index.mjs",
|
|
71
|
+
"require": "./dist/bundle/index.cjs"
|
|
67
72
|
}
|
|
68
73
|
},
|
|
69
74
|
"files": [
|