@hogsend/cli 0.19.0 → 0.20.0

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/bin.js CHANGED
@@ -91,6 +91,10 @@ function createAdminClient(cfg) {
91
91
  body,
92
92
  auth: true
93
93
  }),
94
+ put: (path, body) => request(cfg.baseUrl, cfg.adminKey, missing, "PUT", path, {
95
+ body,
96
+ auth: true
97
+ }),
94
98
  del: (path, body) => request(cfg.baseUrl, cfg.adminKey, missing, "DELETE", path, {
95
99
  body,
96
100
  auth: true
@@ -459,16 +463,796 @@ async function run(ctx) {
459
463
  );
460
464
  }
461
465
  }
462
- var campaignsCommand = {
463
- name: "campaigns",
464
- summary: "Queue a broadcast to a list/bucket, or check its status",
465
- usage,
466
- run
466
+ var campaignsCommand = {
467
+ name: "campaigns",
468
+ summary: "Queue a broadcast to a list/bucket, or check its status",
469
+ usage,
470
+ run
471
+ };
472
+
473
+ // src/commands/connect.ts
474
+ import { parseArgs as parseArgs2 } from "util";
475
+ import { confirm } from "@clack/prompts";
476
+
477
+ // src/lib/browser.ts
478
+ import { spawn } from "child_process";
479
+ function openBrowser(url) {
480
+ const platform = process.platform;
481
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
482
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
483
+ try {
484
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
485
+ child.on("error", () => {
486
+ });
487
+ child.unref();
488
+ return true;
489
+ } catch {
490
+ return false;
491
+ }
492
+ }
493
+
494
+ // src/lib/loopback.ts
495
+ import { createServer } from "http";
496
+
497
+ // src/lib/oauth.ts
498
+ import { createHash, randomBytes } from "crypto";
499
+ var POSTHOG_CLIENT_ID = "https://hogsend.com/.well-known/hogsend-posthog-client.json";
500
+ var POSTHOG_SCOPES = "person:read person:write project:read hog_function:write";
501
+ var LOOPBACK_PORTS = [8423, 8424, 8425];
502
+ var CALLBACK_PATH = "/callback";
503
+ var CALLBACK_TIMEOUT_MS = 3e5;
504
+ var REQUIRED_ACCESS_LEVEL = "team";
505
+ var DISCOVERY_TIMEOUT_MS = 1e4;
506
+ function computeChallenge(verifier) {
507
+ return createHash("sha256").update(verifier, "ascii").digest("base64url");
508
+ }
509
+ function generatePkce() {
510
+ const verifier = randomBytes(32).toString("base64url");
511
+ return { verifier, challenge: computeChallenge(verifier), method: "S256" };
512
+ }
513
+ function generateState() {
514
+ return randomBytes(16).toString("base64url");
515
+ }
516
+ async function discoverOAuthServer(opts) {
517
+ const fetchImpl = opts.fetchImpl ?? fetch;
518
+ const url = `${opts.privateHost}/.well-known/oauth-authorization-server`;
519
+ let res;
520
+ try {
521
+ res = await fetchImpl(url, {
522
+ headers: { Accept: "application/json" },
523
+ signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS)
524
+ });
525
+ } catch (cause) {
526
+ const msg = cause instanceof Error ? cause.message : String(cause);
527
+ return { status: "error", message: `OAuth discovery failed: ${msg}` };
528
+ }
529
+ if (res.status === 404 || res.status === 410) {
530
+ return { status: "unsupported" };
531
+ }
532
+ if (!res.ok) {
533
+ return {
534
+ status: "error",
535
+ message: `OAuth discovery failed (HTTP ${res.status})`
536
+ };
537
+ }
538
+ let doc;
539
+ try {
540
+ doc = await res.json();
541
+ } catch {
542
+ return {
543
+ status: "error",
544
+ message: "malformed discovery document (not JSON)"
545
+ };
546
+ }
547
+ if (typeof doc !== "object" || doc === null || typeof doc.issuer !== "string" || typeof doc.authorization_endpoint !== "string" || typeof doc.token_endpoint !== "string") {
548
+ return {
549
+ status: "error",
550
+ message: "malformed discovery document (missing issuer / authorization_endpoint / token_endpoint)"
551
+ };
552
+ }
553
+ return { status: "ok", metadata: doc };
554
+ }
555
+ function buildAuthorizeUrl(opts) {
556
+ const url = new URL(opts.authorizationEndpoint);
557
+ url.searchParams.set("response_type", "code");
558
+ url.searchParams.set("client_id", opts.clientId);
559
+ url.searchParams.set("redirect_uri", opts.redirectUri);
560
+ url.searchParams.set("scope", opts.scope);
561
+ url.searchParams.set("state", opts.state);
562
+ url.searchParams.set("code_challenge", opts.pkce.challenge);
563
+ url.searchParams.set("code_challenge_method", opts.pkce.method);
564
+ if (opts.requiredAccessLevel !== void 0) {
565
+ url.searchParams.set("required_access_level", opts.requiredAccessLevel);
566
+ }
567
+ return url.toString();
568
+ }
569
+ async function exchangeCode(opts) {
570
+ const fetchImpl = opts.fetchImpl ?? fetch;
571
+ const res = await fetchImpl(opts.tokenEndpoint, {
572
+ method: "POST",
573
+ headers: {
574
+ // URLSearchParams sets this automatically; explicit for clarity.
575
+ "Content-Type": "application/x-www-form-urlencoded",
576
+ Accept: "application/json"
577
+ },
578
+ body: new URLSearchParams({
579
+ grant_type: "authorization_code",
580
+ code: opts.code,
581
+ redirect_uri: opts.redirectUri,
582
+ client_id: opts.clientId,
583
+ code_verifier: opts.codeVerifier
584
+ })
585
+ });
586
+ if (!res.ok) {
587
+ const text3 = await res.text().catch(() => "");
588
+ let detail = text3;
589
+ try {
590
+ const parsed = JSON.parse(text3);
591
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.error === "string") {
592
+ detail = parsed.error;
593
+ }
594
+ } catch {
595
+ }
596
+ throw new Error(
597
+ `token exchange failed (${res.status})${detail ? `: ${detail}` : ""}`
598
+ );
599
+ }
600
+ const json2 = await res.json();
601
+ if (typeof json2.access_token !== "string" || json2.access_token === "") {
602
+ throw new Error("token response missing access_token");
603
+ }
604
+ if (typeof json2.refresh_token !== "string" || json2.refresh_token === "") {
605
+ throw new Error(
606
+ "token response missing refresh_token \u2014 cannot store a long-lived credential"
607
+ );
608
+ }
609
+ return json2;
610
+ }
611
+
612
+ // src/lib/loopback.ts
613
+ var LoopbackError = class extends Error {
614
+ reason;
615
+ detail;
616
+ constructor(reason, message, detail) {
617
+ super(message);
618
+ this.name = "LoopbackError";
619
+ this.reason = reason;
620
+ this.detail = detail;
621
+ }
622
+ };
623
+ var page = (title, body) => `<!doctype html><html><head><meta charset="utf-8"><title>Hogsend</title><style>body{font-family:system-ui,sans-serif;max-width:32rem;margin:6rem auto;padding:0 1rem;color:#1a1a1a}</style></head><body><h1>${title}</h1><p>${body}</p></body></html>`;
624
+ var SUCCESS_HTML = page(
625
+ "Connected",
626
+ "You can close this tab and return to your terminal."
627
+ );
628
+ var DENIED_HTML = page(
629
+ "Not connected",
630
+ "Authorization was denied. Close this tab and re-run the command if that was a mistake."
631
+ );
632
+ var MISMATCH_HTML = page(
633
+ "Not connected",
634
+ "State mismatch. Close this tab and re-run the command."
635
+ );
636
+ var NO_CODE_HTML = page(
637
+ "Not connected",
638
+ "The callback carried no authorization code. Close this tab and re-run the command."
639
+ );
640
+ function tryListen(port, handler) {
641
+ return new Promise((resolve3, reject) => {
642
+ const server = createServer(handler);
643
+ const onError = (err) => {
644
+ server.close();
645
+ if (err.code === "EADDRINUSE") resolve3(null);
646
+ else reject(err);
647
+ };
648
+ server.once("error", onError);
649
+ server.listen({ host: "127.0.0.1", port }, () => {
650
+ server.removeListener("error", onError);
651
+ resolve3(server);
652
+ });
653
+ });
654
+ }
655
+ async function startLoopbackServer(opts) {
656
+ const callbackPath = opts.callbackPath ?? CALLBACK_PATH;
657
+ let settled = false;
658
+ let settleResolve;
659
+ let settleReject;
660
+ const settlePromise = new Promise((resolve3, reject) => {
661
+ settleResolve = resolve3;
662
+ settleReject = reject;
663
+ });
664
+ settlePromise.catch(() => {
665
+ });
666
+ const handler = (req, res) => {
667
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
668
+ if (url.pathname !== callbackPath) {
669
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
670
+ res.end("Not found");
671
+ return;
672
+ }
673
+ if (settled) {
674
+ res.writeHead(410, { "content-type": "text/plain; charset=utf-8" });
675
+ res.end("This callback was already handled \u2014 return to your terminal.");
676
+ return;
677
+ }
678
+ settled = true;
679
+ const respond = (status, html) => {
680
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
681
+ res.end(html);
682
+ };
683
+ const error = url.searchParams.get("error");
684
+ if (error !== null) {
685
+ respond(200, DENIED_HTML);
686
+ const detail = url.searchParams.get("error_description") ?? error;
687
+ settleReject(
688
+ error === "access_denied" ? new LoopbackError(
689
+ "consent_denied",
690
+ "authorization was denied in PostHog",
691
+ detail
692
+ ) : new LoopbackError(
693
+ "oauth_error",
694
+ `the OAuth callback returned an error: ${error}`,
695
+ detail
696
+ )
697
+ );
698
+ return;
699
+ }
700
+ const state = url.searchParams.get("state");
701
+ if (state === null || state !== opts.state) {
702
+ respond(400, MISMATCH_HTML);
703
+ settleReject(
704
+ new LoopbackError(
705
+ "state_mismatch",
706
+ "state mismatch on the OAuth callback \u2014 possible CSRF; retry the command"
707
+ )
708
+ );
709
+ return;
710
+ }
711
+ const code = url.searchParams.get("code");
712
+ if (code === null || code === "") {
713
+ respond(400, NO_CODE_HTML);
714
+ settleReject(
715
+ new LoopbackError(
716
+ "oauth_error",
717
+ "the OAuth callback carried no authorization code"
718
+ )
719
+ );
720
+ return;
721
+ }
722
+ respond(200, SUCCESS_HTML);
723
+ settleResolve({ code });
724
+ };
725
+ let server = null;
726
+ for (const port2 of opts.ports) {
727
+ server = await tryListen(port2, handler);
728
+ if (server) break;
729
+ }
730
+ if (!server) {
731
+ throw new LoopbackError(
732
+ "ports_busy",
733
+ `ports ${opts.ports.join(", ")} on 127.0.0.1 are all in use`
734
+ );
735
+ }
736
+ const address = server.address();
737
+ const port = address !== null && typeof address === "object" ? address.port : 0;
738
+ const bound = server;
739
+ return {
740
+ port,
741
+ redirectUri: `http://127.0.0.1:${port}${callbackPath}`,
742
+ waitForCallback(waitOpts) {
743
+ const timeoutMs = waitOpts?.timeoutMs ?? CALLBACK_TIMEOUT_MS;
744
+ let timer2;
745
+ const timeout = new Promise((_, reject) => {
746
+ timer2 = setTimeout(() => {
747
+ reject(
748
+ new LoopbackError(
749
+ "timeout",
750
+ "timed out waiting for the OAuth callback (5 minutes)"
751
+ )
752
+ );
753
+ }, timeoutMs);
754
+ });
755
+ return Promise.race([settlePromise, timeout]).finally(() => {
756
+ clearTimeout(timer2);
757
+ });
758
+ },
759
+ close() {
760
+ return new Promise((resolve3) => {
761
+ bound.closeAllConnections();
762
+ bound.close(() => resolve3());
763
+ });
764
+ }
765
+ };
766
+ }
767
+
768
+ // src/lib/connect-flow.ts
769
+ var ConnectError = class extends Error {
770
+ verdict;
771
+ hint;
772
+ constructor(verdict, message, hint) {
773
+ super(message);
774
+ this.name = "ConnectError";
775
+ this.verdict = verdict;
776
+ this.hint = hint;
777
+ }
778
+ };
779
+ var HINT_NOT_CONFIGURED = "Set POSTHOG_API_KEY (and POSTHOG_HOST for EU/self-hosted) on the instance, redeploy, then re-run. The server's PostHog config tells the CLI which region to authorize against.";
780
+ var hintOauthUnsupported = (privateHost) => `${privateHost} doesn't advertise an OAuth server (discovery returned 404).
781
+ Self-hosted PostHog builds may not ship OAuth. Use a personal API key instead:
782
+
783
+ 1. In PostHog: Settings -> User -> Personal API keys -> create a key scoped
784
+ person:read, person:write, project:read, hog_function:write
785
+ 2. Set POSTHOG_PERSONAL_API_KEY=<key> on your Hogsend instance (api + worker)
786
+ 3. Redeploy \u2014 person reads and loop provisioning use the key automatically.`;
787
+ var HINT_PORTS = "Ports 8423-8425 on 127.0.0.1 are all in use \u2014 free one and re-run. The OAuth callback must land on one of these fixed ports; they are registered in Hogsend's OAuth client document.";
788
+ var SSH_NOTE = `The consent page must open in a browser on THIS machine \u2014 the OAuth callback
789
+ returns to 127.0.0.1 here. On a remote/SSH session this cannot complete: run
790
+ the command from your laptop instead and point --url at the instance (the CLI
791
+ never needs to run on the server).`;
792
+ function isLoopbackUrl(publicUrl) {
793
+ try {
794
+ const host = new URL(publicUrl).hostname.toLowerCase();
795
+ return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "[::1]" || host === "::1" || host.endsWith(".localhost");
796
+ } catch {
797
+ return false;
798
+ }
799
+ }
800
+ var LOOPBACK_URL_NOTE = `Credential stored \u2014 but this instance's API_PUBLIC_URL is a loopback
801
+ address, so PostHog Cloud cannot deliver webhooks to it. Provisioning was
802
+ skipped (a destination pointing at localhost would be unreachable).
803
+
804
+ Once deployed, wire the loop against the real instance:
805
+
806
+ hogsend connect posthog --provision-only --url https://your-instance`;
807
+ var WEBHOOK_SECRET_NOTE = `Credential stored \u2014 but the PostHog -> Hogsend event loop needs a shared
808
+ webhook secret, and this instance doesn't have one yet. Finish the loop:
809
+
810
+ 1. Generate a secret: openssl rand -hex 32
811
+ 2. Set it on the instance: POSTHOG_WEBHOOK_SECRET=<secret> (api AND worker)
812
+ 3. Redeploy, then run: hogsend connect posthog --provision-only`;
813
+ var errMsg = (err) => err instanceof Error ? err.message : String(err);
814
+ var httpErrorBody = (err) => {
815
+ if (!isHttpError(err)) return void 0;
816
+ const body = err.body;
817
+ if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
818
+ return body.error;
819
+ }
820
+ return void 0;
821
+ };
822
+ function fromLoopbackError(err) {
823
+ switch (err.reason) {
824
+ case "consent_denied":
825
+ return new ConnectError(
826
+ "consent_denied",
827
+ "authorization was denied in PostHog \u2014 re-run the command if that was a mistake"
828
+ );
829
+ case "state_mismatch":
830
+ return new ConnectError(
831
+ "state_mismatch",
832
+ "state mismatch on the OAuth callback \u2014 possible CSRF; retry the command"
833
+ );
834
+ case "timeout":
835
+ return new ConnectError(
836
+ "callback_timeout",
837
+ "timed out waiting for the OAuth callback (5 minutes) \u2014 re-run when you're ready to approve in the browser"
838
+ );
839
+ case "ports_busy":
840
+ return new ConnectError("port_unavailable", err.message, HINT_PORTS);
841
+ case "oauth_error":
842
+ return new ConnectError("exchange_failed", err.message);
843
+ }
844
+ }
845
+ async function runProvisionOnly(deps, info, base) {
846
+ if (info.webhookSecretConfigured === false) {
847
+ deps.out.note(WEBHOOK_SECRET_NOTE, "Webhook secret missing");
848
+ throw new ConnectError(
849
+ "webhook_secret_missing",
850
+ "POSTHOG_WEBHOOK_SECRET is not set on the instance \u2014 nothing to provision against"
851
+ );
852
+ }
853
+ if (isLoopbackUrl(info.apiPublicUrl)) {
854
+ deps.out.note(LOOPBACK_URL_NOTE, "Instance not publicly reachable");
855
+ throw new ConnectError(
856
+ "api_public_url_unreachable",
857
+ `API_PUBLIC_URL is ${info.apiPublicUrl} \u2014 PostHog cannot deliver webhooks to a loopback address`
858
+ );
859
+ }
860
+ let result;
861
+ try {
862
+ result = await deps.out.step(
863
+ `POST ${base}/v1/admin/analytics/provision-loop`,
864
+ () => deps.http.post(
865
+ "/v1/admin/analytics/provision-loop",
866
+ {}
867
+ )
868
+ );
869
+ } catch (err) {
870
+ if (isHttpError(err) && err.status === 409 && httpErrorBody(err) === "no_posthog_credential") {
871
+ throw new ConnectError(
872
+ "no_credential",
873
+ "no PostHog credential is stored on this instance",
874
+ "run `hogsend connect posthog` first"
875
+ );
876
+ }
877
+ throw new ConnectError("provision_failed", errMsg(err));
878
+ }
879
+ printProvisioned(deps.out, result);
880
+ return {
881
+ verdict: "connected",
882
+ providerId: "posthog",
883
+ instance: base,
884
+ posthog: null,
885
+ credential: { stored: false },
886
+ provision: {
887
+ attempted: true,
888
+ ok: true,
889
+ created: result.created === true,
890
+ hogFunctionId: result.hogFunctionId ?? "",
891
+ webhookUrl: result.webhookUrl ?? ""
892
+ }
893
+ };
894
+ }
895
+ function printProvisioned(out, result) {
896
+ out.note(
897
+ [
898
+ "PostHog -> Hogsend loop provisioned",
899
+ ` webhookUrl ${result.webhookUrl ?? "(unknown)"}`,
900
+ ` hogFunctionId ${result.hogFunctionId ?? "(unknown)"}`,
901
+ ` created ${result.created === true ? "yes" : "no (existing function adopted)"}`
902
+ ].join("\n")
903
+ );
904
+ }
905
+ async function runConnectPosthog(deps, opts) {
906
+ const base = deps.http.cfg.baseUrl;
907
+ const info = await deps.out.step(
908
+ `GET ${base}/v1/admin/analytics/connect-info`,
909
+ () => deps.http.get("/v1/admin/analytics/connect-info")
910
+ );
911
+ const privateHost = info.privateHost;
912
+ if (privateHost === null) {
913
+ throw new ConnectError(
914
+ "not_configured",
915
+ "this instance has no PostHog configuration",
916
+ HINT_NOT_CONFIGURED
917
+ );
918
+ }
919
+ if (opts.provisionOnly) {
920
+ return runProvisionOnly(deps, info, base);
921
+ }
922
+ if (info.hostExplicit === false) {
923
+ if (deps.interactive) {
924
+ const proceed = await deps.confirm(
925
+ `No POSTHOG_HOST set on the instance \u2014 assume PostHog US Cloud (${privateHost})?`
926
+ );
927
+ if (!proceed) {
928
+ throw new ConnectError(
929
+ "not_configured",
930
+ "set POSTHOG_HOST on the instance to pick the right region"
931
+ );
932
+ }
933
+ } else {
934
+ deps.out.log(
935
+ `warning: no POSTHOG_HOST set on the instance \u2014 assuming PostHog US Cloud (${privateHost}).`
936
+ );
937
+ }
938
+ }
939
+ if (info.personalKeyConfigured === true) {
940
+ deps.out.log(
941
+ "note: POSTHOG_PERSONAL_API_KEY is set on the instance; the OAuth credential will take precedence once stored."
942
+ );
943
+ }
944
+ const metadata = await deps.out.step(
945
+ `OAuth discovery at ${privateHost}`,
946
+ async () => {
947
+ const result = await deps.discover({ privateHost });
948
+ if (result.status === "unsupported") {
949
+ throw new ConnectError(
950
+ "oauth_unsupported",
951
+ `${privateHost} doesn't advertise an OAuth server (discovery returned 404)`,
952
+ hintOauthUnsupported(privateHost)
953
+ );
954
+ }
955
+ if (result.status === "error") {
956
+ throw new ConnectError("discovery_failed", result.message);
957
+ }
958
+ return result.metadata;
959
+ }
960
+ );
961
+ try {
962
+ if (new URL(metadata.issuer).origin !== new URL(privateHost).origin) {
963
+ deps.out.log(
964
+ `warning: discovery issuer ${metadata.issuer} differs from ${privateHost} \u2014 continuing.`
965
+ );
966
+ }
967
+ } catch {
968
+ }
969
+ const pkce = generatePkce();
970
+ const state = generateState();
971
+ let server;
972
+ try {
973
+ server = await deps.startLoopback({ ports: LOOPBACK_PORTS, state });
974
+ } catch (err) {
975
+ if (err instanceof LoopbackError) throw fromLoopbackError(err);
976
+ throw err;
977
+ }
978
+ let code;
979
+ try {
980
+ const authorizeUrl = buildAuthorizeUrl({
981
+ authorizationEndpoint: metadata.authorization_endpoint,
982
+ clientId: POSTHOG_CLIENT_ID,
983
+ redirectUri: server.redirectUri,
984
+ scope: POSTHOG_SCOPES,
985
+ state,
986
+ pkce,
987
+ requiredAccessLevel: REQUIRED_ACCESS_LEVEL
988
+ });
989
+ deps.out.note(
990
+ [
991
+ "About to authorize Hogsend against PostHog",
992
+ ` instance ${base}`,
993
+ ` posthog ${privateHost}`,
994
+ ` scopes ${POSTHOG_SCOPES}`,
995
+ ` callback ${server.redirectUri}`
996
+ ].join("\n")
997
+ );
998
+ const opened = opts.noBrowser ? false : deps.openBrowser(authorizeUrl);
999
+ deps.out.log(
1000
+ opened ? "Opening your browser. If nothing happens, open this URL yourself:" : "Open this URL in a browser on THIS machine:"
1001
+ );
1002
+ deps.out.log(` ${authorizeUrl}`);
1003
+ if (!opened) {
1004
+ deps.out.note(SSH_NOTE);
1005
+ }
1006
+ const callback = await deps.out.step(
1007
+ "Waiting for PostHog authorization (Ctrl-C aborts)",
1008
+ () => server.waitForCallback({ timeoutMs: opts.timeoutMs })
1009
+ );
1010
+ code = callback.code;
1011
+ } catch (err) {
1012
+ if (err instanceof LoopbackError) throw fromLoopbackError(err);
1013
+ throw err;
1014
+ } finally {
1015
+ await server.close();
1016
+ }
1017
+ const tokenEndpoint = metadata.token_endpoint;
1018
+ let tokens;
1019
+ try {
1020
+ tokens = await deps.out.step(
1021
+ `Exchanging code at ${tokenEndpoint}`,
1022
+ () => deps.exchangeCode({
1023
+ tokenEndpoint,
1024
+ clientId: POSTHOG_CLIENT_ID,
1025
+ code,
1026
+ codeVerifier: pkce.verifier,
1027
+ redirectUri: server.redirectUri
1028
+ })
1029
+ );
1030
+ } catch (err) {
1031
+ throw new ConnectError("exchange_failed", errMsg(err));
1032
+ }
1033
+ const expiresAt = new Date(
1034
+ deps.now().getTime() + tokens.expires_in * 1e3
1035
+ ).toISOString();
1036
+ const scopes = (tokens.scope ?? POSTHOG_SCOPES).split(" ");
1037
+ const scopedTeams = tokens.scoped_teams ?? [];
1038
+ const scopedOrganizations = tokens.scoped_organizations ?? [];
1039
+ try {
1040
+ await deps.out.step(
1041
+ `PUT ${base}/v1/admin/provider-credentials/posthog`,
1042
+ () => deps.http.put("/v1/admin/provider-credentials/posthog", {
1043
+ kind: "oauth",
1044
+ payload: {
1045
+ accessToken: tokens.access_token,
1046
+ refreshToken: tokens.refresh_token,
1047
+ expiresAt,
1048
+ tokenEndpoint,
1049
+ clientId: POSTHOG_CLIENT_ID,
1050
+ scopes,
1051
+ scopedTeams,
1052
+ scopedOrganizations
1053
+ }
1054
+ })
1055
+ );
1056
+ } catch (err) {
1057
+ throw new ConnectError("store_failed", errMsg(err));
1058
+ }
1059
+ const stored = {
1060
+ providerId: "posthog",
1061
+ instance: base,
1062
+ posthog: {
1063
+ privateHost,
1064
+ issuer: metadata.issuer,
1065
+ scopes: scopes.join(" "),
1066
+ scopedTeams,
1067
+ scopedOrganizations
1068
+ },
1069
+ credential: { stored: true, expiresAt }
1070
+ };
1071
+ if (opts.noProvision) {
1072
+ return {
1073
+ verdict: "connected_no_provision",
1074
+ ...stored,
1075
+ provision: { attempted: false, skipped: "no_provision_flag" }
1076
+ };
1077
+ }
1078
+ if (info.webhookSecretConfigured === false) {
1079
+ deps.out.note(WEBHOOK_SECRET_NOTE, "Webhook secret missing");
1080
+ return {
1081
+ verdict: "connected_no_provision",
1082
+ ...stored,
1083
+ provision: { attempted: false, skipped: "webhook_secret_missing" }
1084
+ };
1085
+ }
1086
+ if (isLoopbackUrl(info.apiPublicUrl)) {
1087
+ deps.out.note(LOOPBACK_URL_NOTE, "Instance not publicly reachable");
1088
+ return {
1089
+ verdict: "connected_no_provision",
1090
+ ...stored,
1091
+ provision: { attempted: false, skipped: "api_public_url_unreachable" }
1092
+ };
1093
+ }
1094
+ try {
1095
+ const result = await deps.out.step(
1096
+ `POST ${base}/v1/admin/analytics/provision-loop`,
1097
+ () => deps.http.post(
1098
+ "/v1/admin/analytics/provision-loop",
1099
+ {}
1100
+ )
1101
+ );
1102
+ printProvisioned(deps.out, result);
1103
+ return {
1104
+ verdict: "connected",
1105
+ ...stored,
1106
+ provision: {
1107
+ attempted: true,
1108
+ ok: true,
1109
+ created: result.created === true,
1110
+ hogFunctionId: result.hogFunctionId ?? "",
1111
+ webhookUrl: result.webhookUrl ?? ""
1112
+ }
1113
+ };
1114
+ } catch (err) {
1115
+ const message = errMsg(err);
1116
+ deps.out.log(
1117
+ `The credential is stored, but provisioning the event loop failed: ${message}. Re-run with: hogsend connect posthog --provision-only`
1118
+ );
1119
+ return {
1120
+ verdict: "connected_no_provision",
1121
+ ...stored,
1122
+ provision: { attempted: true, ok: false, error: message }
1123
+ };
1124
+ }
1125
+ }
1126
+
1127
+ // src/lib/prompt.ts
1128
+ import { cancel as cancel2, isCancel } from "@clack/prompts";
1129
+ function bail(value) {
1130
+ if (isCancel(value)) {
1131
+ cancel2("Cancelled.");
1132
+ process.exit(0);
1133
+ }
1134
+ return value;
1135
+ }
1136
+
1137
+ // src/commands/connect.ts
1138
+ var usage2 = `hogsend connect <provider> [--provision-only] [--no-provision] [--no-browser] [--json]
1139
+
1140
+ Connect this Hogsend instance to an analytics provider via OAuth. Providers:
1141
+
1142
+ posthog Authorize Hogsend against your PostHog region (PKCE, loopback
1143
+ callback on 127.0.0.1), store the refresh token on the instance,
1144
+ then provision the PostHog -> Hogsend event loop (a PostHog
1145
+ destination posting to /v1/webhooks/posthog).
1146
+
1147
+ The browser consent must happen on THIS machine (the OAuth callback lands on
1148
+ 127.0.0.1). The target instance can be anywhere \u2014 point --url at it and run
1149
+ this command from your laptop, not from an SSH session on the server.
1150
+
1151
+ Options:
1152
+ --provision-only Skip OAuth; (re-)provision the event loop using the
1153
+ already-stored credential.
1154
+ --no-provision Stop after storing the credential.
1155
+ --no-browser Don't spawn a browser; just print the authorize URL.
1156
+ --url, --admin-key, --json, -h, --help Global flags as usual.
1157
+
1158
+ Exit code: 0 when a credential is stored (even if provisioning was skipped),
1159
+ 1 otherwise.`;
1160
+ async function run2(ctx) {
1161
+ const { values: values2, positionals } = parseArgs2({
1162
+ args: ctx.argv,
1163
+ allowPositionals: true,
1164
+ strict: false,
1165
+ options: {
1166
+ "provision-only": { type: "boolean", default: false },
1167
+ "no-provision": { type: "boolean", default: false },
1168
+ "no-browser": { type: "boolean", default: false },
1169
+ help: { type: "boolean", short: "h", default: false }
1170
+ }
1171
+ });
1172
+ if (values2.help) {
1173
+ ctx.out.log(usage2);
1174
+ return;
1175
+ }
1176
+ const provider = positionals[0];
1177
+ if (!provider) {
1178
+ ctx.out.fail("missing provider \u2014 try: hogsend connect posthog");
1179
+ }
1180
+ if (provider !== "posthog") {
1181
+ ctx.out.fail(`unknown provider "${provider}" \u2014 supported: posthog`);
1182
+ }
1183
+ if (values2["provision-only"] && values2["no-provision"]) {
1184
+ ctx.out.fail("--provision-only and --no-provision are mutually exclusive");
1185
+ }
1186
+ try {
1187
+ const target = new URL(ctx.cfg.baseUrl);
1188
+ if (target.protocol === "http:" && target.hostname !== "localhost" && target.hostname !== "127.0.0.1") {
1189
+ ctx.out.log(
1190
+ color.yellow(
1191
+ `warning: ${ctx.cfg.baseUrl} is plain http \u2014 OAuth tokens will be sent to it unencrypted; use https for remote instances.`
1192
+ )
1193
+ );
1194
+ }
1195
+ } catch {
1196
+ }
1197
+ ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} connect`);
1198
+ const deps = {
1199
+ http: ctx.http,
1200
+ out: ctx.out,
1201
+ interactive: ctx.out.interactive,
1202
+ discover: discoverOAuthServer,
1203
+ startLoopback: startLoopbackServer,
1204
+ exchangeCode,
1205
+ openBrowser,
1206
+ confirm: async (message) => bail(await confirm({ message })),
1207
+ now: () => /* @__PURE__ */ new Date()
1208
+ };
1209
+ try {
1210
+ const result = await runConnectPosthog(deps, {
1211
+ provisionOnly: Boolean(values2["provision-only"]),
1212
+ noProvision: Boolean(values2["no-provision"]),
1213
+ noBrowser: Boolean(values2["no-browser"])
1214
+ });
1215
+ if (ctx.json) {
1216
+ ctx.out.json({ ok: true, ...result });
1217
+ return;
1218
+ }
1219
+ ctx.out.outro(
1220
+ color.green(
1221
+ `connect: posthog ${result.verdict === "connected" ? "connected" : "connected (loop not provisioned)"}`
1222
+ )
1223
+ );
1224
+ } catch (error) {
1225
+ if (error instanceof ConnectError) {
1226
+ if (ctx.json) {
1227
+ ctx.out.json({
1228
+ ok: false,
1229
+ verdict: error.verdict,
1230
+ error: error.message,
1231
+ hint: error.hint
1232
+ });
1233
+ process.exit(1);
1234
+ }
1235
+ ctx.out.note(
1236
+ error.hint ? `${error.message}
1237
+
1238
+ ${error.hint}` : error.message
1239
+ );
1240
+ ctx.out.outro(color.red(`connect: ${error.verdict}`));
1241
+ process.exit(1);
1242
+ }
1243
+ throw error;
1244
+ }
1245
+ }
1246
+ var connectCommand = {
1247
+ name: "connect",
1248
+ summary: "Connect an analytics provider via OAuth (posthog)",
1249
+ usage: usage2,
1250
+ run: run2
467
1251
  };
468
1252
 
469
1253
  // src/commands/contacts.ts
470
- import { parseArgs as parseArgs2 } from "util";
471
- var usage2 = `hogsend contacts <subcommand> [options]
1254
+ import { parseArgs as parseArgs3 } from "util";
1255
+ var usage3 = `hogsend contacts <subcommand> [options]
472
1256
 
473
1257
  Inspect contacts via the admin API (/v1/admin/contacts) and upsert them via the
474
1258
  data plane (PUT /v1/contacts).
@@ -573,7 +1357,7 @@ async function fetchOrFail(ctx, label, fn) {
573
1357
  }
574
1358
  }
575
1359
  async function runList(ctx, argv) {
576
- const { values: values2 } = parseArgs2({
1360
+ const { values: values2 } = parseArgs3({
577
1361
  args: argv,
578
1362
  allowPositionals: true,
579
1363
  options: {
@@ -584,7 +1368,7 @@ async function runList(ctx, argv) {
584
1368
  }
585
1369
  });
586
1370
  if (values2.help) {
587
- ctx.out.log(usage2);
1371
+ ctx.out.log(usage3);
588
1372
  return;
589
1373
  }
590
1374
  const query = {
@@ -616,7 +1400,7 @@ async function runList(ctx, argv) {
616
1400
  );
617
1401
  }
618
1402
  async function runGet(ctx, argv) {
619
- const { values: values2, positionals } = parseArgs2({
1403
+ const { values: values2, positionals } = parseArgs3({
620
1404
  args: argv,
621
1405
  allowPositionals: true,
622
1406
  options: {
@@ -624,7 +1408,7 @@ async function runGet(ctx, argv) {
624
1408
  }
625
1409
  });
626
1410
  if (values2.help) {
627
- ctx.out.log(usage2);
1411
+ ctx.out.log(usage3);
628
1412
  return;
629
1413
  }
630
1414
  const id = positionals[1];
@@ -671,7 +1455,7 @@ async function runGet(ctx, argv) {
671
1455
  ctx.out.outro(`Contact ${color.cyan(contact.externalId)}`);
672
1456
  }
673
1457
  async function runTimeline(ctx, argv) {
674
- const { values: values2, positionals } = parseArgs2({
1458
+ const { values: values2, positionals } = parseArgs3({
675
1459
  args: argv,
676
1460
  allowPositionals: true,
677
1461
  options: {
@@ -682,7 +1466,7 @@ async function runTimeline(ctx, argv) {
682
1466
  }
683
1467
  });
684
1468
  if (values2.help) {
685
- ctx.out.log(usage2);
1469
+ ctx.out.log(usage3);
686
1470
  return;
687
1471
  }
688
1472
  const id = positionals[1];
@@ -736,7 +1520,7 @@ function summarizeTimelineEntry(entry) {
736
1520
  return `${subject} [${String(d.status ?? "")}]`;
737
1521
  }
738
1522
  async function runUpsert(ctx, argv) {
739
- const { values: values2 } = parseArgs2({
1523
+ const { values: values2 } = parseArgs3({
740
1524
  args: argv,
741
1525
  allowPositionals: true,
742
1526
  options: {
@@ -750,7 +1534,7 @@ async function runUpsert(ctx, argv) {
750
1534
  }
751
1535
  });
752
1536
  if (values2.help) {
753
- ctx.out.log(usage2);
1537
+ ctx.out.log(usage3);
754
1538
  return;
755
1539
  }
756
1540
  const email = values2.email;
@@ -790,7 +1574,7 @@ async function runUpsert(ctx, argv) {
790
1574
  const verb = res.created ? "created" : "updated";
791
1575
  ctx.out.outro(`Contact ${color.cyan(res.id)} ${verb}.`);
792
1576
  }
793
- async function run2(ctx) {
1577
+ async function run3(ctx) {
794
1578
  const sub = ctx.argv[0];
795
1579
  switch (sub) {
796
1580
  case "list":
@@ -815,21 +1599,21 @@ async function run2(ctx) {
815
1599
  var contactsCommand = {
816
1600
  name: "contacts",
817
1601
  summary: "List, inspect, trace, and upsert contacts",
818
- usage: usage2,
819
- run: run2
1602
+ usage: usage3,
1603
+ run: run3
820
1604
  };
821
1605
 
822
1606
  // src/commands/dev.ts
823
1607
  import { spawnSync as spawnSync2 } from "child_process";
824
1608
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
825
1609
  import { join as join3, resolve } from "path";
826
- import { parseArgs as parseArgs5 } from "util";
1610
+ import { parseArgs as parseArgs6 } from "util";
827
1611
 
828
1612
  // src/lib/proc.ts
829
- import { spawn } from "child_process";
1613
+ import { spawn as spawn2 } from "child_process";
830
1614
  import { createInterface } from "readline";
831
1615
  function spawnManaged(opts) {
832
- const child = spawn(opts.cmd, opts.args, {
1616
+ const child = spawn2(opts.cmd, opts.args, {
833
1617
  cwd: opts.cwd,
834
1618
  env: { ...process.env, FORCE_COLOR: "1", ...opts.env },
835
1619
  stdio: ["ignore", "pipe", "pipe"],
@@ -930,7 +1714,7 @@ async function waitForHttp(url, timeoutMs) {
930
1714
 
931
1715
  // src/lib/setup-steps.ts
932
1716
  import { spawnSync } from "child_process";
933
- import { randomBytes } from "crypto";
1717
+ import { randomBytes as randomBytes2 } from "crypto";
934
1718
  import { copyFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
935
1719
  import { connect } from "net";
936
1720
  import { join as join2 } from "path";
@@ -938,10 +1722,10 @@ import { join as join2 } from "path";
938
1722
  // src/lib/config.ts
939
1723
  import { existsSync, readFileSync } from "fs";
940
1724
  import { join } from "path";
941
- import { parseArgs as parseArgs3 } from "util";
1725
+ import { parseArgs as parseArgs4 } from "util";
942
1726
  var DEFAULT_BASE_URL = "http://localhost:3002";
943
1727
  function parseGlobalFlags(argv) {
944
- const { values: values2, tokens } = parseArgs3({
1728
+ const { values: values2, tokens } = parseArgs4({
945
1729
  args: argv,
946
1730
  allowPositionals: true,
947
1731
  strict: false,
@@ -1022,7 +1806,7 @@ var SECRET_KEY = "BETTER_AUTH_SECRET";
1022
1806
  var PLACEHOLDER_PREFIX = "change-me";
1023
1807
  var PLACEHOLDER_PREFIXES = [PLACEHOLDER_PREFIX, "REPLACE_ME"];
1024
1808
  function generateSecret() {
1025
- return randomBytes(32).toString("hex");
1809
+ return randomBytes2(32).toString("hex");
1026
1810
  }
1027
1811
  var COMPOSE_FILES = [
1028
1812
  "docker-compose.yml",
@@ -1215,8 +1999,8 @@ async function detectRunningInfra(cwd) {
1215
1999
  }
1216
2000
 
1217
2001
  // src/commands/events.ts
1218
- import { parseArgs as parseArgs4 } from "util";
1219
- var usage3 = `hogsend events <userId> [options]
2002
+ import { parseArgs as parseArgs5 } from "util";
2003
+ var usage4 = `hogsend events <userId> [options]
1220
2004
  hogsend events send <name> [options]
1221
2005
 
1222
2006
  Read a single user's event history (admin API), or send an event into the data
@@ -1262,14 +2046,14 @@ Examples:
1262
2046
  hogsend events user_123 --from 2026-01-01T00:00:00Z --json
1263
2047
  hogsend events send signup --user-id user_123 --prop plan=pro
1264
2048
  hogsend events send purchase --email a@b.com --props '{"amount":49}' --json`;
1265
- async function run3(ctx) {
2049
+ async function run4(ctx) {
1266
2050
  if (ctx.argv[0] === "send") {
1267
2051
  return runSend2(ctx, ctx.argv.slice(1));
1268
2052
  }
1269
2053
  return runRead(ctx, ctx.argv);
1270
2054
  }
1271
2055
  async function runRead(ctx, argv) {
1272
- const { values: values2, positionals } = parseArgs4({
2056
+ const { values: values2, positionals } = parseArgs5({
1273
2057
  args: argv,
1274
2058
  allowPositionals: true,
1275
2059
  options: {
@@ -1282,7 +2066,7 @@ async function runRead(ctx, argv) {
1282
2066
  }
1283
2067
  });
1284
2068
  if (values2.help) {
1285
- ctx.out.log(usage3);
2069
+ ctx.out.log(usage4);
1286
2070
  return;
1287
2071
  }
1288
2072
  const userId = positionals[0];
@@ -1338,7 +2122,7 @@ async function runRead(ctx, argv) {
1338
2122
  );
1339
2123
  }
1340
2124
  async function runSend2(ctx, argv) {
1341
- const { values: values2, positionals } = parseArgs4({
2125
+ const { values: values2, positionals } = parseArgs5({
1342
2126
  args: argv,
1343
2127
  allowPositionals: true,
1344
2128
  options: {
@@ -1356,7 +2140,7 @@ async function runSend2(ctx, argv) {
1356
2140
  }
1357
2141
  });
1358
2142
  if (values2.help) {
1359
- ctx.out.log(usage3);
2143
+ ctx.out.log(usage4);
1360
2144
  return;
1361
2145
  }
1362
2146
  const name = positionals[0];
@@ -1494,12 +2278,12 @@ function summarizeProps(props) {
1494
2278
  var eventsCommand = {
1495
2279
  name: "events",
1496
2280
  summary: "Stream a user's event history, or send an event",
1497
- usage: usage3,
1498
- run: run3
2281
+ usage: usage4,
2282
+ run: run4
1499
2283
  };
1500
2284
 
1501
2285
  // src/commands/dev.ts
1502
- var usage4 = `hogsend dev [options]
2286
+ var usage5 = `hogsend dev [options]
1503
2287
  hogsend dev --fire <event> [event-send options]
1504
2288
 
1505
2289
  Run the full local stack for a Hogsend app from one command:
@@ -1582,7 +2366,7 @@ function extractFire(argv) {
1582
2366
  }
1583
2367
  async function runFire(ctx, event, rest) {
1584
2368
  if (rest.includes("-h") || rest.includes("--help")) {
1585
- ctx.out.log(usage4);
2369
+ ctx.out.log(usage5);
1586
2370
  return;
1587
2371
  }
1588
2372
  try {
@@ -1690,7 +2474,7 @@ async function prepareInfra(ctx, cwd, pkg) {
1690
2474
  ctx.out.log(color.dim(" no db:migrate script \u2014 skipping migrations"));
1691
2475
  }
1692
2476
  }
1693
- async function run4(ctx) {
2477
+ async function run5(ctx) {
1694
2478
  const fire = extractFire(ctx.argv);
1695
2479
  if (fire && "error" in fire) {
1696
2480
  ctx.out.fail(fire.error);
@@ -1699,7 +2483,7 @@ async function run4(ctx) {
1699
2483
  await runFire(ctx, fire.event, fire.rest);
1700
2484
  return;
1701
2485
  }
1702
- const { values: values2 } = parseArgs5({
2486
+ const { values: values2 } = parseArgs6({
1703
2487
  args: ctx.argv,
1704
2488
  allowPositionals: true,
1705
2489
  options: {
@@ -1710,7 +2494,7 @@ async function run4(ctx) {
1710
2494
  }
1711
2495
  });
1712
2496
  if (values2.help) {
1713
- ctx.out.log(usage4);
2497
+ ctx.out.log(usage5);
1714
2498
  return;
1715
2499
  }
1716
2500
  const cwd = resolve(values2.cwd ?? process.cwd());
@@ -1817,12 +2601,12 @@ ${color.dim("Shutting down\u2026")}`);
1817
2601
  var devCommand = {
1818
2602
  name: "dev",
1819
2603
  summary: "Run the full local stack: infra, API + worker, health, URLs",
1820
- usage: usage4,
1821
- run: run4
2604
+ usage: usage5,
2605
+ run: run5
1822
2606
  };
1823
2607
 
1824
2608
  // src/commands/doctor.ts
1825
- import { parseArgs as parseArgs6 } from "util";
2609
+ import { parseArgs as parseArgs7 } from "util";
1826
2610
 
1827
2611
  // src/lib/skills.ts
1828
2612
  import {
@@ -1973,7 +2757,7 @@ function analyticsNudge(ctx) {
1973
2757
  "PostHog person reads disabled"
1974
2758
  );
1975
2759
  }
1976
- var usage5 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
2760
+ var usage6 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
1977
2761
 
1978
2762
  Probe a running Hogsend instance via GET /v1/health and report its health:
1979
2763
  component status (database, redis), two-track schema state (engine + client),
@@ -2015,8 +2799,8 @@ function trackLine(name, track) {
2015
2799
  const required = track.required ?? color.dim("none");
2016
2800
  return `${color.bold(name.padEnd(7))} applied ${applied} -> required ${required} ${sync}`;
2017
2801
  }
2018
- async function run5(ctx) {
2019
- const { values: values2 } = parseArgs6({
2802
+ async function run6(ctx) {
2803
+ const { values: values2 } = parseArgs7({
2020
2804
  args: ctx.argv,
2021
2805
  allowPositionals: true,
2022
2806
  options: {
@@ -2026,7 +2810,7 @@ async function run5(ctx) {
2026
2810
  strict: false
2027
2811
  });
2028
2812
  if (values2.help) {
2029
- ctx.out.log(usage5);
2813
+ ctx.out.log(usage6);
2030
2814
  return;
2031
2815
  }
2032
2816
  const { baseUrl } = ctx.http.cfg;
@@ -2117,13 +2901,13 @@ async function run5(ctx) {
2117
2901
  var doctorCommand = {
2118
2902
  name: "doctor",
2119
2903
  summary: "Probe a running instance's health (GET /v1/health)",
2120
- usage: usage5,
2121
- run: run5
2904
+ usage: usage6,
2905
+ run: run6
2122
2906
  };
2123
2907
 
2124
2908
  // src/commands/domain.ts
2125
- import { parseArgs as parseArgs7 } from "util";
2126
- import { confirm } from "@clack/prompts";
2909
+ import { parseArgs as parseArgs8 } from "util";
2910
+ import { confirm as confirm2 } from "@clack/prompts";
2127
2911
 
2128
2912
  // src/lib/dns.ts
2129
2913
  import { resolveNs as nodeResolveNs } from "dns/promises";
@@ -2417,18 +3201,8 @@ async function applyRecords(opts) {
2417
3201
  return applyVercel(opts, fetchImpl);
2418
3202
  }
2419
3203
 
2420
- // src/lib/prompt.ts
2421
- import { cancel as cancel2, isCancel } from "@clack/prompts";
2422
- function bail(value) {
2423
- if (isCancel(value)) {
2424
- cancel2("Cancelled.");
2425
- process.exit(0);
2426
- }
2427
- return value;
2428
- }
2429
-
2430
3204
  // src/commands/domain.ts
2431
- var usage6 = `hogsend domain <subcommand> [options]
3205
+ var usage7 = `hogsend domain <subcommand> [options]
2432
3206
 
2433
3207
  Manage the sending domain through the RUNNING instance's admin routes
2434
3208
  (/v1/admin/domain) \u2014 provider API keys never touch the CLI. Requires an admin
@@ -2536,7 +3310,7 @@ function renderStatus(ctx, status) {
2536
3310
  }
2537
3311
  }
2538
3312
  async function runAdd(ctx, argv) {
2539
- const { values: values2, positionals } = parseArgs7({
3313
+ const { values: values2, positionals } = parseArgs8({
2540
3314
  args: argv,
2541
3315
  allowPositionals: true,
2542
3316
  options: {
@@ -2546,7 +3320,7 @@ async function runAdd(ctx, argv) {
2546
3320
  }
2547
3321
  });
2548
3322
  if (values2.help) {
2549
- ctx.out.log(usage6);
3323
+ ctx.out.log(usage7);
2550
3324
  return;
2551
3325
  }
2552
3326
  const domain = positionals[0];
@@ -2572,7 +3346,7 @@ async function runAdd(ctx, argv) {
2572
3346
  const autoAvailable = canAutoApply(host.id, process.env);
2573
3347
  if (autoAvailable && records.length > 0) {
2574
3348
  const doApply = values2.apply ? true : values2["no-apply"] ? false : ctx.out.interactive ? bail(
2575
- await confirm({
3349
+ await confirm2({
2576
3350
  message: `Apply these records via the ${host.label} API?`,
2577
3351
  initialValue: true
2578
3352
  })
@@ -2621,7 +3395,7 @@ async function runAdd(ctx, argv) {
2621
3395
  );
2622
3396
  }
2623
3397
  async function runCheck(ctx, argv) {
2624
- const { values: values2, positionals } = parseArgs7({
3398
+ const { values: values2, positionals } = parseArgs8({
2625
3399
  args: argv,
2626
3400
  allowPositionals: true,
2627
3401
  options: {
@@ -2631,7 +3405,7 @@ async function runCheck(ctx, argv) {
2631
3405
  }
2632
3406
  });
2633
3407
  if (values2.help) {
2634
- ctx.out.log(usage6);
3408
+ ctx.out.log(usage7);
2635
3409
  return;
2636
3410
  }
2637
3411
  const timeoutSecs = Number(values2.timeout);
@@ -2703,7 +3477,7 @@ async function runCheck(ctx, argv) {
2703
3477
  }
2704
3478
  }
2705
3479
  async function runStatus2(ctx, argv) {
2706
- const { values: values2 } = parseArgs7({
3480
+ const { values: values2 } = parseArgs8({
2707
3481
  args: argv,
2708
3482
  allowPositionals: false,
2709
3483
  options: {
@@ -2712,7 +3486,7 @@ async function runStatus2(ctx, argv) {
2712
3486
  }
2713
3487
  });
2714
3488
  if (values2.help) {
2715
- ctx.out.log(usage6);
3489
+ ctx.out.log(usage7);
2716
3490
  return;
2717
3491
  }
2718
3492
  const status = await getStatus(ctx, { refresh: values2.refresh });
@@ -2732,11 +3506,11 @@ async function runStatus2(ctx, argv) {
2732
3506
  }
2733
3507
  ctx.out.outro("Done.");
2734
3508
  }
2735
- async function run6(ctx) {
3509
+ async function run7(ctx) {
2736
3510
  const sub = ctx.argv[0];
2737
3511
  const rest = ctx.argv.slice(1);
2738
3512
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
2739
- ctx.out.log(usage6);
3513
+ ctx.out.log(usage7);
2740
3514
  return;
2741
3515
  }
2742
3516
  switch (sub) {
@@ -2755,15 +3529,15 @@ async function run6(ctx) {
2755
3529
  var domainCommand = {
2756
3530
  name: "domain",
2757
3531
  summary: "Set up + verify the sending domain (DNS records, auto-apply)",
2758
- usage: usage6,
2759
- run: run6
3532
+ usage: usage7,
3533
+ run: run7
2760
3534
  };
2761
3535
 
2762
3536
  // src/commands/eject.ts
2763
3537
  import { existsSync as existsSync6, realpathSync } from "fs";
2764
3538
  import { createRequire as createRequire2 } from "module";
2765
3539
  import { dirname, join as join6, sep as sep2 } from "path";
2766
- import { parseArgs as parseArgs8 } from "util";
3540
+ import { parseArgs as parseArgs9 } from "util";
2767
3541
 
2768
3542
  // src/eject.ts
2769
3543
  import { existsSync as existsSync5 } from "fs";
@@ -2877,7 +3651,7 @@ async function countFiles(dir) {
2877
3651
  }
2878
3652
 
2879
3653
  // src/commands/eject.ts
2880
- var usage7 = `hogsend eject <package> [--force] [--cwd <dir>]
3654
+ var usage8 = `hogsend eject <package> [--force] [--cwd <dir>]
2881
3655
 
2882
3656
  Copy a @hogsend/* package's source into vendor/<name> and rewrite the consumer
2883
3657
  dependency to file:./vendor/<name>. Every other dependency keeps upgrading.
@@ -2905,8 +3679,8 @@ function resolveSourceDir(pkg, consumerRoot) {
2905
3679
  }
2906
3680
  return null;
2907
3681
  }
2908
- async function run7(ctx) {
2909
- const { values: values2, positionals } = parseArgs8({
3682
+ async function run8(ctx) {
3683
+ const { values: values2, positionals } = parseArgs9({
2910
3684
  args: ctx.argv,
2911
3685
  allowPositionals: true,
2912
3686
  options: {
@@ -2916,7 +3690,7 @@ async function run7(ctx) {
2916
3690
  }
2917
3691
  });
2918
3692
  if (values2.help) {
2919
- ctx.out.log(usage7);
3693
+ ctx.out.log(usage8);
2920
3694
  return;
2921
3695
  }
2922
3696
  const pkg = positionals[0];
@@ -2960,13 +3734,13 @@ async function run7(ctx) {
2960
3734
  var ejectCommand = {
2961
3735
  name: "eject",
2962
3736
  summary: "Vendor a @hogsend/* package into vendor/<name>",
2963
- usage: usage7,
2964
- run: run7
3737
+ usage: usage8,
3738
+ run: run8
2965
3739
  };
2966
3740
 
2967
3741
  // src/commands/emails.ts
2968
- import { parseArgs as parseArgs9 } from "util";
2969
- var usage8 = `hogsend emails <subcommand> [options]
3742
+ import { parseArgs as parseArgs10 } from "util";
3743
+ var usage9 = `hogsend emails <subcommand> [options]
2970
3744
 
2971
3745
  Send a transactional email through the data plane (POST /v1/emails). The send
2972
3746
  runs through the full preferences + tracking pipeline (link-click + open).
@@ -3044,7 +3818,7 @@ function statusColor2(status) {
3044
3818
  }
3045
3819
  }
3046
3820
  async function runSend3(ctx, argv) {
3047
- const { values: values2, positionals } = parseArgs9({
3821
+ const { values: values2, positionals } = parseArgs10({
3048
3822
  args: argv,
3049
3823
  allowPositionals: true,
3050
3824
  options: {
@@ -3062,7 +3836,7 @@ async function runSend3(ctx, argv) {
3062
3836
  }
3063
3837
  });
3064
3838
  if (values2.help) {
3065
- ctx.out.log(usage8);
3839
+ ctx.out.log(usage9);
3066
3840
  return;
3067
3841
  }
3068
3842
  const template = positionals[0];
@@ -3118,7 +3892,7 @@ async function runSend3(ctx, argv) {
3118
3892
  );
3119
3893
  ctx.out.outro(`${template} \u2192 ${statusColor2(res.status)}.`);
3120
3894
  }
3121
- async function run8(ctx) {
3895
+ async function run9(ctx) {
3122
3896
  const sub = ctx.argv[0];
3123
3897
  switch (sub) {
3124
3898
  case "send":
@@ -3135,12 +3909,12 @@ async function run8(ctx) {
3135
3909
  var emailsCommand = {
3136
3910
  name: "emails",
3137
3911
  summary: "Send a transactional email through the data plane",
3138
- usage: usage8,
3139
- run: run8
3912
+ usage: usage9,
3913
+ run: run9
3140
3914
  };
3141
3915
 
3142
3916
  // src/commands/hatchet.ts
3143
- import { parseArgs as parseArgs10 } from "util";
3917
+ import { parseArgs as parseArgs11 } from "util";
3144
3918
 
3145
3919
  // src/lib/hatchet-token.ts
3146
3920
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -3311,7 +4085,7 @@ async function mintHatchetToken(opts) {
3311
4085
  }
3312
4086
 
3313
4087
  // src/commands/hatchet.ts
3314
- var usage9 = `hogsend hatchet token [options]
4088
+ var usage10 = `hogsend hatchet token [options]
3315
4089
 
3316
4090
  Mint a Hatchet API token (HATCHET_CLIENT_TOKEN) headlessly against a
3317
4091
  hatchet-lite instance. Registers the account if the instance still allows
@@ -3349,7 +4123,7 @@ public URL can create an account. On a public deployment set
3349
4123
  SERVER_ALLOW_SIGNUP=false (plus a real ADMIN_EMAIL/ADMIN_PASSWORD, which
3350
4124
  hatchet-lite seeds at boot); this command then logs in with those credentials.`;
3351
4125
  function parseTokenFlags(argv) {
3352
- const { values: values2 } = parseArgs10({
4126
+ const { values: values2 } = parseArgs11({
3353
4127
  args: argv,
3354
4128
  allowPositionals: true,
3355
4129
  strict: false,
@@ -3415,15 +4189,15 @@ async function runToken(ctx, argv) {
3415
4189
  throw err;
3416
4190
  }
3417
4191
  }
3418
- async function run9(ctx) {
4192
+ async function run10(ctx) {
3419
4193
  const sub = ctx.argv[0];
3420
4194
  const rest = ctx.argv.slice(1);
3421
4195
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
3422
- ctx.out.log(usage9);
4196
+ ctx.out.log(usage10);
3423
4197
  return;
3424
4198
  }
3425
4199
  if (rest.includes("-h") || rest.includes("--help")) {
3426
- ctx.out.log(usage9);
4200
+ ctx.out.log(usage10);
3427
4201
  return;
3428
4202
  }
3429
4203
  switch (sub) {
@@ -3436,13 +4210,13 @@ async function run9(ctx) {
3436
4210
  var hatchetCommand = {
3437
4211
  name: "hatchet",
3438
4212
  summary: "Hatchet helpers \u2014 mint a HATCHET_CLIENT_TOKEN headlessly",
3439
- usage: usage9,
3440
- run: run9
4213
+ usage: usage10,
4214
+ run: run10
3441
4215
  };
3442
4216
 
3443
4217
  // src/commands/journeys.ts
3444
- import { parseArgs as parseArgs11 } from "util";
3445
- var usage10 = `hogsend journeys <subcommand> [options]
4218
+ import { parseArgs as parseArgs12 } from "util";
4219
+ var usage11 = `hogsend journeys <subcommand> [options]
3446
4220
 
3447
4221
  Inspect and toggle journeys via the admin API (/v1/admin/journeys).
3448
4222
 
@@ -3471,7 +4245,7 @@ function statusColor3(enabled) {
3471
4245
  return enabled ? color.green("enabled") : color.yellow("disabled");
3472
4246
  }
3473
4247
  async function runList2(ctx) {
3474
- const { values: values2 } = parseArgs11({
4248
+ const { values: values2 } = parseArgs12({
3475
4249
  args: ctx.argv,
3476
4250
  allowPositionals: true,
3477
4251
  options: {
@@ -3482,7 +4256,7 @@ async function runList2(ctx) {
3482
4256
  }
3483
4257
  });
3484
4258
  if (values2.help) {
3485
- ctx.out.log(usage10);
4259
+ ctx.out.log(usage11);
3486
4260
  return;
3487
4261
  }
3488
4262
  if (values2.enabled !== void 0 && !["true", "false"].includes(values2.enabled)) {
@@ -3618,7 +4392,7 @@ async function runToggle(ctx, id, enabled) {
3618
4392
  );
3619
4393
  ctx.out.outro(`${j.id} is now ${statusColor3(j.enabled)}.`);
3620
4394
  }
3621
- async function run10(ctx) {
4395
+ async function run11(ctx) {
3622
4396
  const sub = ctx.argv[0];
3623
4397
  const rest = ctx.argv.slice(1);
3624
4398
  const subCtx = { ...ctx, argv: rest };
@@ -3630,7 +4404,7 @@ async function run10(ctx) {
3630
4404
  case "get": {
3631
4405
  const id = rest.find((a) => !a.startsWith("-"));
3632
4406
  if (rest.includes("--help") || rest.includes("-h")) {
3633
- ctx.out.log(usage10);
4407
+ ctx.out.log(usage11);
3634
4408
  return;
3635
4409
  }
3636
4410
  await runGet2(subCtx, id);
@@ -3638,7 +4412,7 @@ async function run10(ctx) {
3638
4412
  }
3639
4413
  case "enable": {
3640
4414
  if (rest.includes("--help") || rest.includes("-h")) {
3641
- ctx.out.log(usage10);
4415
+ ctx.out.log(usage11);
3642
4416
  return;
3643
4417
  }
3644
4418
  await runToggle(
@@ -3650,7 +4424,7 @@ async function run10(ctx) {
3650
4424
  }
3651
4425
  case "disable": {
3652
4426
  if (rest.includes("--help") || rest.includes("-h")) {
3653
- ctx.out.log(usage10);
4427
+ ctx.out.log(usage11);
3654
4428
  return;
3655
4429
  }
3656
4430
  await runToggle(
@@ -3684,14 +4458,14 @@ async function run10(ctx) {
3684
4458
  var journeysCommand = {
3685
4459
  name: "journeys",
3686
4460
  summary: "List, inspect, enable, and disable journeys",
3687
- usage: usage10,
3688
- run: run10
4461
+ usage: usage11,
4462
+ run: run11
3689
4463
  };
3690
4464
 
3691
4465
  // src/commands/patch.ts
3692
4466
  import { spawnSync as spawnSync3 } from "child_process";
3693
- import { parseArgs as parseArgs12 } from "util";
3694
- var usage11 = `hogsend patch <package> [--cwd <dir>]
4467
+ import { parseArgs as parseArgs13 } from "util";
4468
+ var usage12 = `hogsend patch <package> [--cwd <dir>]
3695
4469
 
3696
4470
  Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
3697
4471
  extracts the package into a temp dir and prints the path to edit. After editing,
@@ -3702,8 +4476,8 @@ This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
3702
4476
  Options:
3703
4477
  --cwd <dir> Project root to run pnpm in (defaults to current directory).
3704
4478
  -h, --help Show this help.`;
3705
- async function run11(ctx) {
3706
- const { values: values2, positionals } = parseArgs12({
4479
+ async function run12(ctx) {
4480
+ const { values: values2, positionals } = parseArgs13({
3707
4481
  args: ctx.argv,
3708
4482
  allowPositionals: true,
3709
4483
  options: {
@@ -3712,7 +4486,7 @@ async function run11(ctx) {
3712
4486
  }
3713
4487
  });
3714
4488
  if (values2.help) {
3715
- ctx.out.log(usage11);
4489
+ ctx.out.log(usage12);
3716
4490
  return;
3717
4491
  }
3718
4492
  const pkg = positionals[0];
@@ -3752,16 +4526,16 @@ async function run11(ctx) {
3752
4526
  var patchCommand = {
3753
4527
  name: "patch",
3754
4528
  summary: "Patch a package via pnpm's native patch flow",
3755
- usage: usage11,
3756
- run: run11
4529
+ usage: usage12,
4530
+ run: run12
3757
4531
  };
3758
4532
 
3759
4533
  // src/commands/setup.ts
3760
4534
  import { existsSync as existsSync7 } from "fs";
3761
4535
  import { join as join7 } from "path";
3762
- import { parseArgs as parseArgs13 } from "util";
3763
- import { confirm as confirm2 } from "@clack/prompts";
3764
- var usage12 = `hogsend setup [--cwd <dir>] [--yes] [--json]
4536
+ import { parseArgs as parseArgs14 } from "util";
4537
+ import { confirm as confirm3 } from "@clack/prompts";
4538
+ var usage13 = `hogsend setup [--cwd <dir>] [--yes] [--json]
3765
4539
 
3766
4540
  Interactive local onboarding for a scaffolded Hogsend app. Mirrors the
3767
4541
  create-hogsend "next steps":
@@ -3778,8 +4552,8 @@ Options:
3778
4552
  -h, --help Show this help.
3779
4553
 
3780
4554
  Run ${color.cyan("hogsend doctor")} afterwards to verify the instance is healthy.`;
3781
- async function run12(ctx) {
3782
- const { values: values2 } = parseArgs13({
4555
+ async function run13(ctx) {
4556
+ const { values: values2 } = parseArgs14({
3783
4557
  args: ctx.argv,
3784
4558
  allowPositionals: true,
3785
4559
  options: {
@@ -3789,7 +4563,7 @@ async function run12(ctx) {
3789
4563
  }
3790
4564
  });
3791
4565
  if (values2.help) {
3792
- ctx.out.log(usage12);
4566
+ ctx.out.log(usage13);
3793
4567
  return;
3794
4568
  }
3795
4569
  const cwd = values2.cwd ?? process.cwd();
@@ -3807,7 +4581,7 @@ async function run12(ctx) {
3807
4581
  }
3808
4582
  if (ctx.out.interactive && !skipConfirm) {
3809
4583
  const proceed = bail(
3810
- await confirm2({
4584
+ await confirm3({
3811
4585
  message: `Set up local infra in ${color.cyan(cwd)}? (docker compose up, .env, db:migrate)`
3812
4586
  })
3813
4587
  );
@@ -3904,16 +4678,16 @@ async function run12(ctx) {
3904
4678
  var setupCommand = {
3905
4679
  name: "setup",
3906
4680
  summary: "Local onboarding: docker compose up, gen secret, db:migrate",
3907
- usage: usage12,
3908
- run: run12
4681
+ usage: usage13,
4682
+ run: run13
3909
4683
  };
3910
4684
 
3911
4685
  // src/commands/skills.ts
3912
4686
  import { existsSync as existsSync8 } from "fs";
3913
4687
  import { join as join8 } from "path";
3914
- import { parseArgs as parseArgs14 } from "util";
4688
+ import { parseArgs as parseArgs15 } from "util";
3915
4689
  import { multiselect } from "@clack/prompts";
3916
- var usage13 = `hogsend skills <subcommand> [options]
4690
+ var usage14 = `hogsend skills <subcommand> [options]
3917
4691
 
3918
4692
  Manage the Claude Code skills bundled with @hogsend/cli. Bundled skills teach
3919
4693
  agents how to drive the hogsend CLI; \`add\` copies them into your project's
@@ -3974,7 +4748,7 @@ function runList3(ctx) {
3974
4748
  );
3975
4749
  }
3976
4750
  async function runAdd2(ctx, argv) {
3977
- const { values: values2, positionals } = parseArgs14({
4751
+ const { values: values2, positionals } = parseArgs15({
3978
4752
  args: argv,
3979
4753
  allowPositionals: true,
3980
4754
  options: {
@@ -3984,7 +4758,7 @@ async function runAdd2(ctx, argv) {
3984
4758
  }
3985
4759
  });
3986
4760
  if (values2.help) {
3987
- ctx.out.log(usage13);
4761
+ ctx.out.log(usage14);
3988
4762
  return;
3989
4763
  }
3990
4764
  const cwd = process.cwd();
@@ -4052,7 +4826,7 @@ async function runAdd2(ctx, argv) {
4052
4826
  `Installed ${installedCount} skill${installedCount === 1 ? "" : "s"}` + (skippedCount > 0 ? `, skipped ${skippedCount}.` : ".")
4053
4827
  );
4054
4828
  }
4055
- async function run13(ctx) {
4829
+ async function run14(ctx) {
4056
4830
  const sub = ctx.argv[0];
4057
4831
  switch (sub) {
4058
4832
  case "list":
@@ -4064,7 +4838,7 @@ async function run13(ctx) {
4064
4838
  case void 0:
4065
4839
  case "-h":
4066
4840
  case "--help":
4067
- ctx.out.log(usage13);
4841
+ ctx.out.log(usage14);
4068
4842
  return;
4069
4843
  default:
4070
4844
  ctx.out.fail(
@@ -4075,13 +4849,13 @@ async function run13(ctx) {
4075
4849
  var skillsCommand = {
4076
4850
  name: "skills",
4077
4851
  summary: "List + install bundled Claude Code skills into .claude/skills",
4078
- usage: usage13,
4079
- run: run13
4852
+ usage: usage14,
4853
+ run: run14
4080
4854
  };
4081
4855
 
4082
4856
  // src/commands/stats.ts
4083
- import { parseArgs as parseArgs15 } from "util";
4084
- var usage14 = `hogsend stats [--json]
4857
+ import { parseArgs as parseArgs16 } from "util";
4858
+ var usage15 = `hogsend stats [--json]
4085
4859
 
4086
4860
  Show system-wide overview metrics from a running Hogsend instance.
4087
4861
  Wraps GET /v1/admin/metrics/overview.
@@ -4103,8 +4877,8 @@ Options:
4103
4877
  function pct(rate) {
4104
4878
  return `${(rate * 100).toFixed(2)}%`;
4105
4879
  }
4106
- async function run14(ctx) {
4107
- const { values: values2 } = parseArgs15({
4880
+ async function run15(ctx) {
4881
+ const { values: values2 } = parseArgs16({
4108
4882
  args: ctx.argv,
4109
4883
  allowPositionals: true,
4110
4884
  options: {
@@ -4112,7 +4886,7 @@ async function run14(ctx) {
4112
4886
  }
4113
4887
  });
4114
4888
  if (values2.help) {
4115
- ctx.out.log(usage14);
4889
+ ctx.out.log(usage15);
4116
4890
  return;
4117
4891
  }
4118
4892
  const metrics = await ctx.out.step(
@@ -4141,20 +4915,19 @@ async function run14(ctx) {
4141
4915
  var statsCommand = {
4142
4916
  name: "stats",
4143
4917
  summary: "Show system-wide overview metrics",
4144
- usage: usage14,
4145
- run: run14
4918
+ usage: usage15,
4919
+ run: run15
4146
4920
  };
4147
4921
 
4148
4922
  // src/commands/studio.ts
4149
- import { spawn as spawn2 } from "child_process";
4150
4923
  import { createReadStream, existsSync as existsSync9, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
4151
- import { createServer } from "http";
4924
+ import { createServer as createServer2 } from "http";
4152
4925
  import { extname, join as join9, normalize, resolve as resolve2, sep as sep3 } from "path";
4153
4926
  import { fileURLToPath as fileURLToPath2 } from "url";
4154
- import { parseArgs as parseArgs17 } from "util";
4927
+ import { parseArgs as parseArgs18 } from "util";
4155
4928
 
4156
4929
  // src/commands/studio-admin.ts
4157
- import { parseArgs as parseArgs16 } from "util";
4930
+ import { parseArgs as parseArgs17 } from "util";
4158
4931
  import { password as passwordPrompt, text as text2 } from "@clack/prompts";
4159
4932
 
4160
4933
  // ../../node_modules/.pnpm/postgres@3.4.9/node_modules/postgres/src/index.js
@@ -13342,6 +14115,7 @@ __export(schema_exports, {
13342
14115
  memberRelations: () => memberRelations,
13343
14116
  organization: () => organization,
13344
14117
  organizationRelations: () => organizationRelations,
14118
+ providerCredentials: () => providerCredentials,
13345
14119
  session: () => session,
13346
14120
  sessionRelations: () => sessionRelations,
13347
14121
  trackedLinks: () => trackedLinks,
@@ -14044,6 +14818,30 @@ var linkClicks = pgTable(
14044
14818
  ]
14045
14819
  );
14046
14820
 
14821
+ // ../db/src/schema/provider-credentials.ts
14822
+ var providerCredentials = pgTable(
14823
+ "provider_credentials",
14824
+ {
14825
+ id: uuid("id").defaultRandom().primaryKey(),
14826
+ // e.g. "posthog" — matches an AnalyticsProvider meta.id by convention,
14827
+ // but deliberately NOT foreign-keyed: providers are code-defined.
14828
+ providerId: text("provider_id").notNull(),
14829
+ // "oauth" today; "api_key" is the anticipated future kind.
14830
+ kind: text("kind").notNull().default("oauth"),
14831
+ // Encrypted JSON: base64url(iv || ciphertext || gcmTag).
14832
+ payload: text("payload").notNull(),
14833
+ ...timestamps
14834
+ },
14835
+ (table) => [
14836
+ // unique-per-kind: one oauth credential per provider; a future api_key
14837
+ // credential for the same provider coexists.
14838
+ uniqueIndex("provider_credentials_provider_kind_idx").on(
14839
+ table.providerId,
14840
+ table.kind
14841
+ )
14842
+ ]
14843
+ );
14844
+
14047
14845
  // ../db/src/schema/user-events.ts
14048
14846
  var userEvents = pgTable(
14049
14847
  "user_events",
@@ -14539,7 +15337,7 @@ Examples:
14539
15337
  Security: passwords are written ONLY via better-auth (scrypt) \u2014 never raw SQL,
14540
15338
  never plaintext at rest, never logged. Prefer the masked prompt over --password.`;
14541
15339
  function parseAdminFlags(argv) {
14542
- const { values: values2 } = parseArgs16({
15340
+ const { values: values2 } = parseArgs17({
14543
15341
  args: argv,
14544
15342
  allowPositionals: true,
14545
15343
  strict: false,
@@ -14740,7 +15538,7 @@ async function runStudioAdmin(ctx, argv) {
14740
15538
  }
14741
15539
 
14742
15540
  // src/commands/studio.ts
14743
- var usage15 = `hogsend studio [options]
15541
+ var usage16 = `hogsend studio [options]
14744
15542
 
14745
15543
  Subcommands:
14746
15544
  admin create | reset | list Shell-gated Studio admin recovery (DB + secret).
@@ -14818,24 +15616,12 @@ function indexHtml(distPath, baseUrl) {
14818
15616
  }
14819
15617
  return `${inject}${raw}`;
14820
15618
  }
14821
- function openBrowser(url) {
14822
- const platform = process.platform;
14823
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
14824
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
14825
- try {
14826
- const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
14827
- child.on("error", () => {
14828
- });
14829
- child.unref();
14830
- } catch {
14831
- }
14832
- }
14833
- async function run15(ctx) {
15619
+ async function run16(ctx) {
14834
15620
  if (ctx.argv[0] === "admin") {
14835
15621
  await runStudioAdmin(ctx, ctx.argv.slice(1));
14836
15622
  return;
14837
15623
  }
14838
- const { values: values2, positionals } = parseArgs17({
15624
+ const { values: values2, positionals } = parseArgs18({
14839
15625
  args: ctx.argv,
14840
15626
  allowPositionals: true,
14841
15627
  strict: false,
@@ -14848,7 +15634,7 @@ async function run15(ctx) {
14848
15634
  }
14849
15635
  });
14850
15636
  if (values2.help) {
14851
- ctx.out.log(usage15);
15637
+ ctx.out.log(usage16);
14852
15638
  return;
14853
15639
  }
14854
15640
  const port = Number(values2.port ?? "3333");
@@ -14866,7 +15652,7 @@ async function run15(ctx) {
14866
15652
  }
14867
15653
  const cleanBase = baseUrl ? baseUrl.replace(/\/+$/, "") : void 0;
14868
15654
  const index2 = indexHtml(distPath, cleanBase);
14869
- const server = createServer((req, res) => {
15655
+ const server = createServer2((req, res) => {
14870
15656
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
14871
15657
  const rel = urlPath.replace(/^\/studio/, "");
14872
15658
  if (rel === "" || rel === "/") {
@@ -14934,17 +15720,17 @@ async function run15(ctx) {
14934
15720
  var studioCommand = {
14935
15721
  name: "studio",
14936
15722
  summary: "Serve the bundled Hogsend Studio admin SPA locally",
14937
- usage: usage15,
14938
- run: run15
15723
+ usage: usage16,
15724
+ run: run16
14939
15725
  };
14940
15726
 
14941
15727
  // src/commands/upgrade.ts
14942
15728
  import { spawnSync as spawnSync4 } from "child_process";
14943
15729
  import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
14944
15730
  import { join as join10 } from "path";
14945
- import { parseArgs as parseArgs18 } from "util";
14946
- import { confirm as confirm3 } from "@clack/prompts";
14947
- var usage16 = `hogsend upgrade [--cwd <dir>] [--pm <pnpm|npm|yarn|bun>] [options]
15731
+ import { parseArgs as parseArgs19 } from "util";
15732
+ import { confirm as confirm4 } from "@clack/prompts";
15733
+ var usage17 = `hogsend upgrade [--cwd <dir>] [--pm <pnpm|npm|yarn|bun>] [options]
14948
15734
 
14949
15735
  Upgrade a scaffolded Hogsend app in one step:
14950
15736
  1. bump every @hogsend/* dependency to latest (or --to <version>), then
@@ -14980,8 +15766,8 @@ function hogsendDeps(cwd) {
14980
15766
  function addArgs(pm, specs) {
14981
15767
  return [pm === "npm" ? "install" : "add", ...specs];
14982
15768
  }
14983
- async function run16(ctx) {
14984
- const { values: values2 } = parseArgs18({
15769
+ async function run17(ctx) {
15770
+ const { values: values2 } = parseArgs19({
14985
15771
  args: ctx.argv,
14986
15772
  allowPositionals: true,
14987
15773
  options: {
@@ -14995,7 +15781,7 @@ async function run16(ctx) {
14995
15781
  }
14996
15782
  });
14997
15783
  if (values2.help) {
14998
- ctx.out.log(usage16);
15784
+ ctx.out.log(usage17);
14999
15785
  return;
15000
15786
  }
15001
15787
  if (values2["deps-only"] && values2["skills-only"]) {
@@ -15039,7 +15825,7 @@ async function run16(ctx) {
15039
15825
  doSkills ? "refresh .claude/skills" : null
15040
15826
  ].filter(Boolean).join(" + ");
15041
15827
  const proceed = bail(
15042
- await confirm3({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
15828
+ await confirm4({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
15043
15829
  );
15044
15830
  if (!proceed) {
15045
15831
  ctx.out.outro(color.dim("Nothing changed."));
@@ -15120,12 +15906,12 @@ async function run16(ctx) {
15120
15906
  var upgradeCommand = {
15121
15907
  name: "upgrade",
15122
15908
  summary: "Bump @hogsend/* deps to latest + refresh vendored skills",
15123
- usage: usage16,
15124
- run: run16
15909
+ usage: usage17,
15910
+ run: run17
15125
15911
  };
15126
15912
 
15127
15913
  // src/commands/webhooks.ts
15128
- import { parseArgs as parseArgs19 } from "util";
15914
+ import { parseArgs as parseArgs20 } from "util";
15129
15915
  var WEBHOOK_EVENT_TYPES = [
15130
15916
  "contact.created",
15131
15917
  "contact.updated",
@@ -15142,7 +15928,7 @@ var WEBHOOK_EVENT_TYPES = [
15142
15928
  "bucket.entered",
15143
15929
  "bucket.left"
15144
15930
  ];
15145
- var usage17 = `hogsend webhooks <subcommand> [options]
15931
+ var usage18 = `hogsend webhooks <subcommand> [options]
15146
15932
 
15147
15933
  Manage outbound webhook endpoints \u2014 the Svix-style signed event stream Hogsend
15148
15934
  emits to your URLs. Wraps the admin routes (/v1/admin/webhooks), so this command
@@ -15232,7 +16018,7 @@ ${color.bold(secret)}`,
15232
16018
  );
15233
16019
  }
15234
16020
  async function runList5(ctx, argv) {
15235
- const { values: values2 } = parseArgs19({
16021
+ const { values: values2 } = parseArgs20({
15236
16022
  args: argv,
15237
16023
  allowPositionals: true,
15238
16024
  options: {
@@ -15243,7 +16029,7 @@ async function runList5(ctx, argv) {
15243
16029
  }
15244
16030
  });
15245
16031
  if (values2.help) {
15246
- ctx.out.log(usage17);
16032
+ ctx.out.log(usage18);
15247
16033
  return;
15248
16034
  }
15249
16035
  const query = {
@@ -15292,13 +16078,13 @@ function renderEndpoint(ctx, ep, title) {
15292
16078
  );
15293
16079
  }
15294
16080
  async function runGet3(ctx, argv) {
15295
- const { values: values2, positionals } = parseArgs19({
16081
+ const { values: values2, positionals } = parseArgs20({
15296
16082
  args: argv,
15297
16083
  allowPositionals: true,
15298
16084
  options: { help: { type: "boolean", short: "h", default: false } }
15299
16085
  });
15300
16086
  if (values2.help) {
15301
- ctx.out.log(usage17);
16087
+ ctx.out.log(usage18);
15302
16088
  return;
15303
16089
  }
15304
16090
  const id = positionals[0];
@@ -15323,7 +16109,7 @@ async function runGet3(ctx, argv) {
15323
16109
  ctx.out.outro(`${res.url} \u2192 ${res.status}`);
15324
16110
  }
15325
16111
  async function runCreate2(ctx, argv) {
15326
- const { values: values2 } = parseArgs19({
16112
+ const { values: values2 } = parseArgs20({
15327
16113
  args: argv,
15328
16114
  allowPositionals: true,
15329
16115
  options: {
@@ -15336,7 +16122,7 @@ async function runCreate2(ctx, argv) {
15336
16122
  }
15337
16123
  });
15338
16124
  if (values2.help) {
15339
- ctx.out.log(usage17);
16125
+ ctx.out.log(usage18);
15340
16126
  return;
15341
16127
  }
15342
16128
  const url = values2.url;
@@ -15370,7 +16156,7 @@ async function runCreate2(ctx, argv) {
15370
16156
  ctx.out.outro(`${color.green("Created")} ${res.id} \u2192 ${res.url}`);
15371
16157
  }
15372
16158
  async function runUpdate(ctx, argv) {
15373
- const { values: values2, positionals } = parseArgs19({
16159
+ const { values: values2, positionals } = parseArgs20({
15374
16160
  args: argv,
15375
16161
  allowPositionals: true,
15376
16162
  options: {
@@ -15384,7 +16170,7 @@ async function runUpdate(ctx, argv) {
15384
16170
  }
15385
16171
  });
15386
16172
  if (values2.help) {
15387
- ctx.out.log(usage17);
16173
+ ctx.out.log(usage18);
15388
16174
  return;
15389
16175
  }
15390
16176
  const id = positionals[0];
@@ -15425,13 +16211,13 @@ async function runUpdate(ctx, argv) {
15425
16211
  ctx.out.outro(`${color.green("Updated")} ${res.id} \u2192 ${res.status}`);
15426
16212
  }
15427
16213
  async function runDelete(ctx, argv) {
15428
- const { values: values2, positionals } = parseArgs19({
16214
+ const { values: values2, positionals } = parseArgs20({
15429
16215
  args: argv,
15430
16216
  allowPositionals: true,
15431
16217
  options: { help: { type: "boolean", short: "h", default: false } }
15432
16218
  });
15433
16219
  if (values2.help) {
15434
- ctx.out.log(usage17);
16220
+ ctx.out.log(usage18);
15435
16221
  return;
15436
16222
  }
15437
16223
  const id = positionals[0];
@@ -15455,13 +16241,13 @@ async function runDelete(ctx, argv) {
15455
16241
  ctx.out.outro(`${color.green("Deleted")} ${id}`);
15456
16242
  }
15457
16243
  async function runRotate(ctx, argv) {
15458
- const { values: values2, positionals } = parseArgs19({
16244
+ const { values: values2, positionals } = parseArgs20({
15459
16245
  args: argv,
15460
16246
  allowPositionals: true,
15461
16247
  options: { help: { type: "boolean", short: "h", default: false } }
15462
16248
  });
15463
16249
  if (values2.help) {
15464
- ctx.out.log(usage17);
16250
+ ctx.out.log(usage18);
15465
16251
  return;
15466
16252
  }
15467
16253
  const id = positionals[0];
@@ -15490,13 +16276,13 @@ async function runRotate(ctx, argv) {
15490
16276
  );
15491
16277
  }
15492
16278
  async function runTest(ctx, argv) {
15493
- const { values: values2, positionals } = parseArgs19({
16279
+ const { values: values2, positionals } = parseArgs20({
15494
16280
  args: argv,
15495
16281
  allowPositionals: true,
15496
16282
  options: { help: { type: "boolean", short: "h", default: false } }
15497
16283
  });
15498
16284
  if (values2.help) {
15499
- ctx.out.log(usage17);
16285
+ ctx.out.log(usage18);
15500
16286
  return;
15501
16287
  }
15502
16288
  const id = positionals[0];
@@ -15522,7 +16308,7 @@ async function runTest(ctx, argv) {
15522
16308
  `${color.green("Enqueued")} a ${color.cyan(res.eventType)} delivery to ${id}.`
15523
16309
  );
15524
16310
  }
15525
- async function run17(ctx) {
16311
+ async function run18(ctx) {
15526
16312
  const sub = ctx.argv[0];
15527
16313
  switch (sub) {
15528
16314
  case "list":
@@ -15553,8 +16339,8 @@ async function run17(ctx) {
15553
16339
  var webhooksCommand = {
15554
16340
  name: "webhooks",
15555
16341
  summary: "Manage outbound webhook endpoints (create, rotate, test)",
15556
- usage: usage17,
15557
- run: run17
16342
+ usage: usage18,
16343
+ run: run18
15558
16344
  };
15559
16345
 
15560
16346
  // src/commands/index.ts
@@ -15568,6 +16354,7 @@ var commands = [
15568
16354
  campaignsCommand,
15569
16355
  webhooksCommand,
15570
16356
  domainCommand,
16357
+ connectCommand,
15571
16358
  hatchetCommand,
15572
16359
  studioCommand,
15573
16360
  devCommand,