@corenel/sidecar 0.1.4 → 0.1.5
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/cli.js +133 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -713,10 +713,20 @@ function extractTunnelUrl(s) {
|
|
|
713
713
|
const m = s.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
714
714
|
return m ? m[0] : null;
|
|
715
715
|
}
|
|
716
|
+
function isRegisteredLine(s) {
|
|
717
|
+
return /Registered tunnel connection/.test(s);
|
|
718
|
+
}
|
|
719
|
+
function isTunnelGoneLine(s) {
|
|
720
|
+
return /Unauthorized: Tunnel not found/i.test(s);
|
|
721
|
+
}
|
|
722
|
+
var GONE_LINE_LIMIT = 3;
|
|
716
723
|
var START_TIMEOUT_MS2 = 25e3;
|
|
717
724
|
function tunnelArgs(port, host = "localhost") {
|
|
718
725
|
return ["tunnel", "--no-autoupdate", "--protocol", "http2", "--url", `http://${host}:${port}`];
|
|
719
726
|
}
|
|
727
|
+
function namedTunnelArgs(name, port, host = "localhost") {
|
|
728
|
+
return ["tunnel", "--no-autoupdate", "--protocol", "http2", "run", "--url", `http://${host}:${port}`, name];
|
|
729
|
+
}
|
|
720
730
|
var tail2 = (s, n = 6) => s.trim().split("\n").slice(-n).join("\n");
|
|
721
731
|
function cloudflaredBins() {
|
|
722
732
|
const env = process.env.CLOUDFLARED_BIN?.trim();
|
|
@@ -747,24 +757,34 @@ function stopChild2(child) {
|
|
|
747
757
|
}
|
|
748
758
|
});
|
|
749
759
|
}
|
|
750
|
-
function startWith2(bin,
|
|
760
|
+
function startWith2(bin, mode, log, onDown) {
|
|
751
761
|
return new Promise((resolveHandle, reject) => {
|
|
752
|
-
const child = spawn3(bin,
|
|
762
|
+
const child = spawn3(bin, mode.argv, { stdio: ["ignore", "pipe", "pipe"] });
|
|
753
763
|
let out = "";
|
|
754
764
|
let settled = false;
|
|
755
765
|
let tailBuf = "";
|
|
766
|
+
let goneLines = 0;
|
|
767
|
+
let downFired = false;
|
|
756
768
|
const finish = (fn) => {
|
|
757
769
|
if (settled) return;
|
|
758
770
|
settled = true;
|
|
759
771
|
clearTimeout(timer);
|
|
760
772
|
fn();
|
|
761
773
|
};
|
|
774
|
+
const fireDown = (reason) => {
|
|
775
|
+
if (downFired) return;
|
|
776
|
+
downFired = true;
|
|
777
|
+
onDown?.(reason);
|
|
778
|
+
};
|
|
762
779
|
const onData = (b) => {
|
|
763
780
|
const text = b.toString();
|
|
764
781
|
if (!settled) {
|
|
765
782
|
out += text;
|
|
766
|
-
const url =
|
|
767
|
-
if (url) finish(() => resolveHandle({ url, stop: () =>
|
|
783
|
+
const url = mode.ready(out);
|
|
784
|
+
if (url) finish(() => resolveHandle({ url, stop: () => {
|
|
785
|
+
downFired = true;
|
|
786
|
+
return stopChild2(child);
|
|
787
|
+
} }));
|
|
768
788
|
return;
|
|
769
789
|
}
|
|
770
790
|
tailBuf += text;
|
|
@@ -772,7 +792,20 @@ function startWith2(bin, port, host, log) {
|
|
|
772
792
|
while ((nl = tailBuf.indexOf("\n")) >= 0) {
|
|
773
793
|
const line = tailBuf.slice(0, nl).replace(/\r$/, "").trim();
|
|
774
794
|
tailBuf = tailBuf.slice(nl + 1);
|
|
775
|
-
if (line)
|
|
795
|
+
if (!line) continue;
|
|
796
|
+
log?.(`cloudflared: ${line}`);
|
|
797
|
+
if (isRegisteredLine(line)) {
|
|
798
|
+
goneLines = 0;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (isTunnelGoneLine(line)) {
|
|
802
|
+
goneLines++;
|
|
803
|
+
if (goneLines >= GONE_LINE_LIMIT && !downFired) {
|
|
804
|
+
log?.(`cloudflared: tunnel deregistered by the edge (${goneLines}x "Tunnel not found") \u2014 it can never re-register; replacing it`);
|
|
805
|
+
void stopChild2(child);
|
|
806
|
+
fireDown("quick tunnel deregistered by the edge");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
776
809
|
}
|
|
777
810
|
};
|
|
778
811
|
child.stdout?.on("data", onData);
|
|
@@ -781,14 +814,15 @@ function startWith2(bin, port, host, log) {
|
|
|
781
814
|
child.on("exit", (code) => {
|
|
782
815
|
if (settled) {
|
|
783
816
|
log?.(`cloudflared exited (code ${code}) \u2014 the tunnel is down`);
|
|
817
|
+
fireDown(`cloudflared exited (code ${code})`);
|
|
784
818
|
return;
|
|
785
819
|
}
|
|
786
|
-
finish(() => reject(new Error(`${bin} exited (code ${code}) before
|
|
820
|
+
finish(() => reject(new Error(`${bin} exited (code ${code}) before the tunnel came up.
|
|
787
821
|
${tail2(out)}`)));
|
|
788
822
|
});
|
|
789
823
|
const timer = setTimeout(() => finish(() => {
|
|
790
824
|
void stopChild2(child);
|
|
791
|
-
reject(new Error(`${bin}
|
|
825
|
+
reject(new Error(`${bin} did not come up within ${START_TIMEOUT_MS2 / 1e3}s.
|
|
792
826
|
${tail2(out)}`));
|
|
793
827
|
}), START_TIMEOUT_MS2);
|
|
794
828
|
});
|
|
@@ -796,8 +830,21 @@ ${tail2(out)}`));
|
|
|
796
830
|
var cloudflaredProvider = {
|
|
797
831
|
id: "cloudflared",
|
|
798
832
|
aliases: ["cloudflare"],
|
|
799
|
-
description: "Cloudflare
|
|
800
|
-
start({ port, host, log }) {
|
|
833
|
+
description: "Cloudflare tunnel (needs the `cloudflared` CLI installed \u2014 https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/). Default: anonymous quick tunnel (no account, ephemeral URL). With --tunnel-name + --tunnel-hostname: your own NAMED tunnel (stable URL, auto-reconnects).",
|
|
834
|
+
start({ port, host, name, hostname, log, onDown }) {
|
|
835
|
+
const origin = host || "localhost";
|
|
836
|
+
let mode;
|
|
837
|
+
if (name) {
|
|
838
|
+
if (!hostname) {
|
|
839
|
+
return Promise.reject(new Error(
|
|
840
|
+
`a named cloudflared tunnel needs --tunnel-hostname <public-host> \u2014 the DNS route you created with \`cloudflared tunnel route dns ${name} <public-host>\` (one-time setup: \`cloudflared tunnel login\` then \`cloudflared tunnel create ${name}\`).`
|
|
841
|
+
));
|
|
842
|
+
}
|
|
843
|
+
const url = `https://${hostname.replace(/^https?:\/\//, "")}`;
|
|
844
|
+
mode = { argv: namedTunnelArgs(name, port, origin), ready: (s) => isRegisteredLine(s) ? url : null };
|
|
845
|
+
} else {
|
|
846
|
+
mode = { argv: tunnelArgs(port, origin), ready: extractTunnelUrl };
|
|
847
|
+
}
|
|
801
848
|
const bins = cloudflaredBins();
|
|
802
849
|
const tryAt = (i) => {
|
|
803
850
|
if (i >= bins.length) {
|
|
@@ -805,7 +852,7 @@ var cloudflaredProvider = {
|
|
|
805
852
|
`cloudflared not found (tried: ${bins.join(", ")}). Install the cloudflared CLI (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/), or set CLOUDFLARED_BIN to the binary name/path.`
|
|
806
853
|
));
|
|
807
854
|
}
|
|
808
|
-
return startWith2(bins[i],
|
|
855
|
+
return startWith2(bins[i], mode, log, onDown).catch((e) => {
|
|
809
856
|
if (e?.code === "ENOENT") return tryAt(i + 1);
|
|
810
857
|
throw e;
|
|
811
858
|
});
|
|
@@ -818,6 +865,66 @@ var cloudflaredProvider = {
|
|
|
818
865
|
registerTunnel(cloudflaredProvider);
|
|
819
866
|
registerTunnel(zrokProvider);
|
|
820
867
|
|
|
868
|
+
// src/tunnel/supervisor.ts
|
|
869
|
+
var DEFAULT_BACKOFF = [2e3, 5e3, 1e4, 3e4];
|
|
870
|
+
async function superviseTunnel(opts) {
|
|
871
|
+
const backoff = opts.backoffMs ?? DEFAULT_BACKOFF;
|
|
872
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
873
|
+
const log = opts.start.log;
|
|
874
|
+
let stopped = false;
|
|
875
|
+
let restarting = false;
|
|
876
|
+
let current = null;
|
|
877
|
+
let incarnation = 0;
|
|
878
|
+
const startOnce = () => {
|
|
879
|
+
const mine = ++incarnation;
|
|
880
|
+
return opts.provider.start({
|
|
881
|
+
...opts.start,
|
|
882
|
+
onDown: (reason) => {
|
|
883
|
+
if (mine === incarnation) void restart(reason);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
};
|
|
887
|
+
async function restart(reason) {
|
|
888
|
+
if (stopped || restarting) return;
|
|
889
|
+
restarting = true;
|
|
890
|
+
current = null;
|
|
891
|
+
log?.(`tunnel down (${reason}) \u2014 restarting`);
|
|
892
|
+
try {
|
|
893
|
+
for (let attempt = 0; !stopped; attempt++) {
|
|
894
|
+
await sleep(backoff[Math.min(attempt, backoff.length - 1)]);
|
|
895
|
+
if (stopped) return;
|
|
896
|
+
try {
|
|
897
|
+
const h = await startOnce();
|
|
898
|
+
if (stopped) {
|
|
899
|
+
void h.stop().catch(() => {
|
|
900
|
+
});
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
current = h;
|
|
904
|
+
opts.onUp(h.url, true);
|
|
905
|
+
return;
|
|
906
|
+
} catch (e) {
|
|
907
|
+
log?.(`tunnel restart failed (${e instanceof Error ? e.message : String(e)}) \u2014 retrying`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
} finally {
|
|
911
|
+
restarting = false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
current = await startOnce();
|
|
915
|
+
opts.onUp(current.url, false);
|
|
916
|
+
return {
|
|
917
|
+
stop: async () => {
|
|
918
|
+
stopped = true;
|
|
919
|
+
incarnation++;
|
|
920
|
+
const c = current;
|
|
921
|
+
current = null;
|
|
922
|
+
if (c) await c.stop().catch(() => {
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
821
928
|
// src/cli.ts
|
|
822
929
|
function parseArgs(argv) {
|
|
823
930
|
const out = { port: 4858, host: "127.0.0.1", roots: [], allowOrigins: [], allowShell: false, allowLan: false, selfSigned: false, open: false, appUrl: "https://prompd.app" };
|
|
@@ -834,6 +941,7 @@ function parseArgs(argv) {
|
|
|
834
941
|
else if (a === "--self-signed") out.selfSigned = true;
|
|
835
942
|
else if (a === "--tunnel") out.tunnel = argv[++i];
|
|
836
943
|
else if (a === "--tunnel-name") out.tunnelName = argv[++i];
|
|
944
|
+
else if (a === "--tunnel-hostname") out.tunnelHostname = argv[++i];
|
|
837
945
|
else if (a === "--open") out.open = true;
|
|
838
946
|
else if (a === "--app-url") out.appUrl = argv[++i];
|
|
839
947
|
else if (a === "--sidecar" || a === "start" || a === "--") {
|
|
@@ -854,6 +962,7 @@ var KNOWN_FLAGS = [
|
|
|
854
962
|
"--self-signed",
|
|
855
963
|
"--tunnel",
|
|
856
964
|
"--tunnel-name",
|
|
965
|
+
"--tunnel-hostname",
|
|
857
966
|
"--open",
|
|
858
967
|
"--app-url"
|
|
859
968
|
];
|
|
@@ -920,13 +1029,22 @@ async function main() {
|
|
|
920
1029
|
log: slog,
|
|
921
1030
|
allowLan: args.allowLan
|
|
922
1031
|
});
|
|
923
|
-
let
|
|
1032
|
+
let tunnelSup = null;
|
|
924
1033
|
let tunnelWss = null;
|
|
925
1034
|
if (tunnelProvider) {
|
|
926
|
-
process.stdout.write(`starting ${tunnelProvider.id} tunnel${args.tunnelName ? ` (
|
|
1035
|
+
process.stdout.write(`starting ${tunnelProvider.id} tunnel${args.tunnelName ? ` (name "${args.tunnelName}")` : ""}\u2026
|
|
927
1036
|
`);
|
|
928
|
-
|
|
929
|
-
|
|
1037
|
+
tunnelSup = await superviseTunnel({
|
|
1038
|
+
provider: tunnelProvider,
|
|
1039
|
+
start: { port: handle.port, host: handle.host, name: args.tunnelName, hostname: args.tunnelHostname, log: slog },
|
|
1040
|
+
onUp: (url, restarted) => {
|
|
1041
|
+
const wss = url.replace(/^http/, "ws");
|
|
1042
|
+
const changed = tunnelWss !== null && wss !== tunnelWss;
|
|
1043
|
+
tunnelWss = wss;
|
|
1044
|
+
if (!restarted) return;
|
|
1045
|
+
slog(changed ? `tunnel restored at a NEW URL: ${wss} \u2014 re-pair the app with this URL (the token is unchanged)` : "tunnel restored \u2014 same URL, existing pairings reconnect automatically");
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
930
1048
|
}
|
|
931
1049
|
const scheme = handle.secure ? "wss" : "ws";
|
|
932
1050
|
const exposed = handle.host !== "127.0.0.1" && handle.host !== "localhost";
|
|
@@ -955,7 +1073,7 @@ async function main() {
|
|
|
955
1073
|
}
|
|
956
1074
|
const shutdown = () => {
|
|
957
1075
|
const closeServer = () => handle.close().then(() => process.exit(0));
|
|
958
|
-
if (
|
|
1076
|
+
if (tunnelSup) void tunnelSup.stop().catch(() => {
|
|
959
1077
|
}).then(closeServer);
|
|
960
1078
|
else void closeServer();
|
|
961
1079
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@corenel/sidecar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",
|