@hogsend/cli 0.18.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 {
@@ -1954,7 +2738,26 @@ function skillsNudge(ctx) {
1954
2738
  "Skills out of date"
1955
2739
  );
1956
2740
  }
1957
- var usage5 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
2741
+ function analyticsNudge(ctx) {
2742
+ if (ctx.json) return;
2743
+ const dotenv = loadDotEnv(process.cwd());
2744
+ const captureKey = process.env.POSTHOG_API_KEY ?? dotenv.POSTHOG_API_KEY;
2745
+ const personalKey = process.env.POSTHOG_PERSONAL_API_KEY ?? dotenv.POSTHOG_PERSONAL_API_KEY;
2746
+ if (!captureKey || personalKey) return;
2747
+ ctx.out.note(
2748
+ [
2749
+ "POSTHOG_API_KEY is set without POSTHOG_PERSONAL_API_KEY \u2014 person",
2750
+ "property READS are disabled (the phc_ project key is write-only by",
2751
+ "PostHog's design), so per-user timezone resolution falls back to",
2752
+ "contact properties. Capture and person WRITES are unaffected.",
2753
+ "",
2754
+ `Fix: create a personal API key scoped ${color.cyan("person:read")} and set ${color.cyan("POSTHOG_PERSONAL_API_KEY")}.`,
2755
+ `Docs: ${color.cyan("https://hogsend.com/docs/guides/analytics-access")}`
2756
+ ].join("\n"),
2757
+ "PostHog person reads disabled"
2758
+ );
2759
+ }
2760
+ var usage6 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
1958
2761
 
1959
2762
  Probe a running Hogsend instance via GET /v1/health and report its health:
1960
2763
  component status (database, redis), two-track schema state (engine + client),
@@ -1996,8 +2799,8 @@ function trackLine(name, track) {
1996
2799
  const required = track.required ?? color.dim("none");
1997
2800
  return `${color.bold(name.padEnd(7))} applied ${applied} -> required ${required} ${sync}`;
1998
2801
  }
1999
- async function run5(ctx) {
2000
- const { values: values2 } = parseArgs6({
2802
+ async function run6(ctx) {
2803
+ const { values: values2 } = parseArgs7({
2001
2804
  args: ctx.argv,
2002
2805
  allowPositionals: true,
2003
2806
  options: {
@@ -2007,7 +2810,7 @@ async function run5(ctx) {
2007
2810
  strict: false
2008
2811
  });
2009
2812
  if (values2.help) {
2010
- ctx.out.log(usage5);
2813
+ ctx.out.log(usage6);
2011
2814
  return;
2012
2815
  }
2013
2816
  const { baseUrl } = ctx.http.cfg;
@@ -2087,6 +2890,7 @@ async function run5(ctx) {
2087
2890
  ];
2088
2891
  ctx.out.note(lines.join("\n"), "Doctor");
2089
2892
  skillsNudge(ctx);
2893
+ analyticsNudge(ctx);
2090
2894
  if (ok) {
2091
2895
  ctx.out.outro(color.green("doctor: ok"));
2092
2896
  return;
@@ -2097,13 +2901,13 @@ async function run5(ctx) {
2097
2901
  var doctorCommand = {
2098
2902
  name: "doctor",
2099
2903
  summary: "Probe a running instance's health (GET /v1/health)",
2100
- usage: usage5,
2101
- run: run5
2904
+ usage: usage6,
2905
+ run: run6
2102
2906
  };
2103
2907
 
2104
2908
  // src/commands/domain.ts
2105
- import { parseArgs as parseArgs7 } from "util";
2106
- import { confirm } from "@clack/prompts";
2909
+ import { parseArgs as parseArgs8 } from "util";
2910
+ import { confirm as confirm2 } from "@clack/prompts";
2107
2911
 
2108
2912
  // src/lib/dns.ts
2109
2913
  import { resolveNs as nodeResolveNs } from "dns/promises";
@@ -2397,18 +3201,8 @@ async function applyRecords(opts) {
2397
3201
  return applyVercel(opts, fetchImpl);
2398
3202
  }
2399
3203
 
2400
- // src/lib/prompt.ts
2401
- import { cancel as cancel2, isCancel } from "@clack/prompts";
2402
- function bail(value) {
2403
- if (isCancel(value)) {
2404
- cancel2("Cancelled.");
2405
- process.exit(0);
2406
- }
2407
- return value;
2408
- }
2409
-
2410
3204
  // src/commands/domain.ts
2411
- var usage6 = `hogsend domain <subcommand> [options]
3205
+ var usage7 = `hogsend domain <subcommand> [options]
2412
3206
 
2413
3207
  Manage the sending domain through the RUNNING instance's admin routes
2414
3208
  (/v1/admin/domain) \u2014 provider API keys never touch the CLI. Requires an admin
@@ -2516,7 +3310,7 @@ function renderStatus(ctx, status) {
2516
3310
  }
2517
3311
  }
2518
3312
  async function runAdd(ctx, argv) {
2519
- const { values: values2, positionals } = parseArgs7({
3313
+ const { values: values2, positionals } = parseArgs8({
2520
3314
  args: argv,
2521
3315
  allowPositionals: true,
2522
3316
  options: {
@@ -2526,7 +3320,7 @@ async function runAdd(ctx, argv) {
2526
3320
  }
2527
3321
  });
2528
3322
  if (values2.help) {
2529
- ctx.out.log(usage6);
3323
+ ctx.out.log(usage7);
2530
3324
  return;
2531
3325
  }
2532
3326
  const domain = positionals[0];
@@ -2552,7 +3346,7 @@ async function runAdd(ctx, argv) {
2552
3346
  const autoAvailable = canAutoApply(host.id, process.env);
2553
3347
  if (autoAvailable && records.length > 0) {
2554
3348
  const doApply = values2.apply ? true : values2["no-apply"] ? false : ctx.out.interactive ? bail(
2555
- await confirm({
3349
+ await confirm2({
2556
3350
  message: `Apply these records via the ${host.label} API?`,
2557
3351
  initialValue: true
2558
3352
  })
@@ -2601,7 +3395,7 @@ async function runAdd(ctx, argv) {
2601
3395
  );
2602
3396
  }
2603
3397
  async function runCheck(ctx, argv) {
2604
- const { values: values2, positionals } = parseArgs7({
3398
+ const { values: values2, positionals } = parseArgs8({
2605
3399
  args: argv,
2606
3400
  allowPositionals: true,
2607
3401
  options: {
@@ -2611,7 +3405,7 @@ async function runCheck(ctx, argv) {
2611
3405
  }
2612
3406
  });
2613
3407
  if (values2.help) {
2614
- ctx.out.log(usage6);
3408
+ ctx.out.log(usage7);
2615
3409
  return;
2616
3410
  }
2617
3411
  const timeoutSecs = Number(values2.timeout);
@@ -2683,7 +3477,7 @@ async function runCheck(ctx, argv) {
2683
3477
  }
2684
3478
  }
2685
3479
  async function runStatus2(ctx, argv) {
2686
- const { values: values2 } = parseArgs7({
3480
+ const { values: values2 } = parseArgs8({
2687
3481
  args: argv,
2688
3482
  allowPositionals: false,
2689
3483
  options: {
@@ -2692,7 +3486,7 @@ async function runStatus2(ctx, argv) {
2692
3486
  }
2693
3487
  });
2694
3488
  if (values2.help) {
2695
- ctx.out.log(usage6);
3489
+ ctx.out.log(usage7);
2696
3490
  return;
2697
3491
  }
2698
3492
  const status = await getStatus(ctx, { refresh: values2.refresh });
@@ -2712,11 +3506,11 @@ async function runStatus2(ctx, argv) {
2712
3506
  }
2713
3507
  ctx.out.outro("Done.");
2714
3508
  }
2715
- async function run6(ctx) {
3509
+ async function run7(ctx) {
2716
3510
  const sub = ctx.argv[0];
2717
3511
  const rest = ctx.argv.slice(1);
2718
3512
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
2719
- ctx.out.log(usage6);
3513
+ ctx.out.log(usage7);
2720
3514
  return;
2721
3515
  }
2722
3516
  switch (sub) {
@@ -2735,15 +3529,15 @@ async function run6(ctx) {
2735
3529
  var domainCommand = {
2736
3530
  name: "domain",
2737
3531
  summary: "Set up + verify the sending domain (DNS records, auto-apply)",
2738
- usage: usage6,
2739
- run: run6
3532
+ usage: usage7,
3533
+ run: run7
2740
3534
  };
2741
3535
 
2742
3536
  // src/commands/eject.ts
2743
3537
  import { existsSync as existsSync6, realpathSync } from "fs";
2744
3538
  import { createRequire as createRequire2 } from "module";
2745
3539
  import { dirname, join as join6, sep as sep2 } from "path";
2746
- import { parseArgs as parseArgs8 } from "util";
3540
+ import { parseArgs as parseArgs9 } from "util";
2747
3541
 
2748
3542
  // src/eject.ts
2749
3543
  import { existsSync as existsSync5 } from "fs";
@@ -2857,7 +3651,7 @@ async function countFiles(dir) {
2857
3651
  }
2858
3652
 
2859
3653
  // src/commands/eject.ts
2860
- var usage7 = `hogsend eject <package> [--force] [--cwd <dir>]
3654
+ var usage8 = `hogsend eject <package> [--force] [--cwd <dir>]
2861
3655
 
2862
3656
  Copy a @hogsend/* package's source into vendor/<name> and rewrite the consumer
2863
3657
  dependency to file:./vendor/<name>. Every other dependency keeps upgrading.
@@ -2885,8 +3679,8 @@ function resolveSourceDir(pkg, consumerRoot) {
2885
3679
  }
2886
3680
  return null;
2887
3681
  }
2888
- async function run7(ctx) {
2889
- const { values: values2, positionals } = parseArgs8({
3682
+ async function run8(ctx) {
3683
+ const { values: values2, positionals } = parseArgs9({
2890
3684
  args: ctx.argv,
2891
3685
  allowPositionals: true,
2892
3686
  options: {
@@ -2896,7 +3690,7 @@ async function run7(ctx) {
2896
3690
  }
2897
3691
  });
2898
3692
  if (values2.help) {
2899
- ctx.out.log(usage7);
3693
+ ctx.out.log(usage8);
2900
3694
  return;
2901
3695
  }
2902
3696
  const pkg = positionals[0];
@@ -2940,13 +3734,13 @@ async function run7(ctx) {
2940
3734
  var ejectCommand = {
2941
3735
  name: "eject",
2942
3736
  summary: "Vendor a @hogsend/* package into vendor/<name>",
2943
- usage: usage7,
2944
- run: run7
3737
+ usage: usage8,
3738
+ run: run8
2945
3739
  };
2946
3740
 
2947
3741
  // src/commands/emails.ts
2948
- import { parseArgs as parseArgs9 } from "util";
2949
- var usage8 = `hogsend emails <subcommand> [options]
3742
+ import { parseArgs as parseArgs10 } from "util";
3743
+ var usage9 = `hogsend emails <subcommand> [options]
2950
3744
 
2951
3745
  Send a transactional email through the data plane (POST /v1/emails). The send
2952
3746
  runs through the full preferences + tracking pipeline (link-click + open).
@@ -3024,7 +3818,7 @@ function statusColor2(status) {
3024
3818
  }
3025
3819
  }
3026
3820
  async function runSend3(ctx, argv) {
3027
- const { values: values2, positionals } = parseArgs9({
3821
+ const { values: values2, positionals } = parseArgs10({
3028
3822
  args: argv,
3029
3823
  allowPositionals: true,
3030
3824
  options: {
@@ -3042,7 +3836,7 @@ async function runSend3(ctx, argv) {
3042
3836
  }
3043
3837
  });
3044
3838
  if (values2.help) {
3045
- ctx.out.log(usage8);
3839
+ ctx.out.log(usage9);
3046
3840
  return;
3047
3841
  }
3048
3842
  const template = positionals[0];
@@ -3098,7 +3892,7 @@ async function runSend3(ctx, argv) {
3098
3892
  );
3099
3893
  ctx.out.outro(`${template} \u2192 ${statusColor2(res.status)}.`);
3100
3894
  }
3101
- async function run8(ctx) {
3895
+ async function run9(ctx) {
3102
3896
  const sub = ctx.argv[0];
3103
3897
  switch (sub) {
3104
3898
  case "send":
@@ -3115,12 +3909,12 @@ async function run8(ctx) {
3115
3909
  var emailsCommand = {
3116
3910
  name: "emails",
3117
3911
  summary: "Send a transactional email through the data plane",
3118
- usage: usage8,
3119
- run: run8
3912
+ usage: usage9,
3913
+ run: run9
3120
3914
  };
3121
3915
 
3122
3916
  // src/commands/hatchet.ts
3123
- import { parseArgs as parseArgs10 } from "util";
3917
+ import { parseArgs as parseArgs11 } from "util";
3124
3918
 
3125
3919
  // src/lib/hatchet-token.ts
3126
3920
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -3291,7 +4085,7 @@ async function mintHatchetToken(opts) {
3291
4085
  }
3292
4086
 
3293
4087
  // src/commands/hatchet.ts
3294
- var usage9 = `hogsend hatchet token [options]
4088
+ var usage10 = `hogsend hatchet token [options]
3295
4089
 
3296
4090
  Mint a Hatchet API token (HATCHET_CLIENT_TOKEN) headlessly against a
3297
4091
  hatchet-lite instance. Registers the account if the instance still allows
@@ -3329,7 +4123,7 @@ public URL can create an account. On a public deployment set
3329
4123
  SERVER_ALLOW_SIGNUP=false (plus a real ADMIN_EMAIL/ADMIN_PASSWORD, which
3330
4124
  hatchet-lite seeds at boot); this command then logs in with those credentials.`;
3331
4125
  function parseTokenFlags(argv) {
3332
- const { values: values2 } = parseArgs10({
4126
+ const { values: values2 } = parseArgs11({
3333
4127
  args: argv,
3334
4128
  allowPositionals: true,
3335
4129
  strict: false,
@@ -3395,15 +4189,15 @@ async function runToken(ctx, argv) {
3395
4189
  throw err;
3396
4190
  }
3397
4191
  }
3398
- async function run9(ctx) {
4192
+ async function run10(ctx) {
3399
4193
  const sub = ctx.argv[0];
3400
4194
  const rest = ctx.argv.slice(1);
3401
4195
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
3402
- ctx.out.log(usage9);
4196
+ ctx.out.log(usage10);
3403
4197
  return;
3404
4198
  }
3405
4199
  if (rest.includes("-h") || rest.includes("--help")) {
3406
- ctx.out.log(usage9);
4200
+ ctx.out.log(usage10);
3407
4201
  return;
3408
4202
  }
3409
4203
  switch (sub) {
@@ -3416,13 +4210,13 @@ async function run9(ctx) {
3416
4210
  var hatchetCommand = {
3417
4211
  name: "hatchet",
3418
4212
  summary: "Hatchet helpers \u2014 mint a HATCHET_CLIENT_TOKEN headlessly",
3419
- usage: usage9,
3420
- run: run9
4213
+ usage: usage10,
4214
+ run: run10
3421
4215
  };
3422
4216
 
3423
4217
  // src/commands/journeys.ts
3424
- import { parseArgs as parseArgs11 } from "util";
3425
- var usage10 = `hogsend journeys <subcommand> [options]
4218
+ import { parseArgs as parseArgs12 } from "util";
4219
+ var usage11 = `hogsend journeys <subcommand> [options]
3426
4220
 
3427
4221
  Inspect and toggle journeys via the admin API (/v1/admin/journeys).
3428
4222
 
@@ -3451,7 +4245,7 @@ function statusColor3(enabled) {
3451
4245
  return enabled ? color.green("enabled") : color.yellow("disabled");
3452
4246
  }
3453
4247
  async function runList2(ctx) {
3454
- const { values: values2 } = parseArgs11({
4248
+ const { values: values2 } = parseArgs12({
3455
4249
  args: ctx.argv,
3456
4250
  allowPositionals: true,
3457
4251
  options: {
@@ -3462,7 +4256,7 @@ async function runList2(ctx) {
3462
4256
  }
3463
4257
  });
3464
4258
  if (values2.help) {
3465
- ctx.out.log(usage10);
4259
+ ctx.out.log(usage11);
3466
4260
  return;
3467
4261
  }
3468
4262
  if (values2.enabled !== void 0 && !["true", "false"].includes(values2.enabled)) {
@@ -3598,7 +4392,7 @@ async function runToggle(ctx, id, enabled) {
3598
4392
  );
3599
4393
  ctx.out.outro(`${j.id} is now ${statusColor3(j.enabled)}.`);
3600
4394
  }
3601
- async function run10(ctx) {
4395
+ async function run11(ctx) {
3602
4396
  const sub = ctx.argv[0];
3603
4397
  const rest = ctx.argv.slice(1);
3604
4398
  const subCtx = { ...ctx, argv: rest };
@@ -3610,7 +4404,7 @@ async function run10(ctx) {
3610
4404
  case "get": {
3611
4405
  const id = rest.find((a) => !a.startsWith("-"));
3612
4406
  if (rest.includes("--help") || rest.includes("-h")) {
3613
- ctx.out.log(usage10);
4407
+ ctx.out.log(usage11);
3614
4408
  return;
3615
4409
  }
3616
4410
  await runGet2(subCtx, id);
@@ -3618,7 +4412,7 @@ async function run10(ctx) {
3618
4412
  }
3619
4413
  case "enable": {
3620
4414
  if (rest.includes("--help") || rest.includes("-h")) {
3621
- ctx.out.log(usage10);
4415
+ ctx.out.log(usage11);
3622
4416
  return;
3623
4417
  }
3624
4418
  await runToggle(
@@ -3630,7 +4424,7 @@ async function run10(ctx) {
3630
4424
  }
3631
4425
  case "disable": {
3632
4426
  if (rest.includes("--help") || rest.includes("-h")) {
3633
- ctx.out.log(usage10);
4427
+ ctx.out.log(usage11);
3634
4428
  return;
3635
4429
  }
3636
4430
  await runToggle(
@@ -3664,14 +4458,14 @@ async function run10(ctx) {
3664
4458
  var journeysCommand = {
3665
4459
  name: "journeys",
3666
4460
  summary: "List, inspect, enable, and disable journeys",
3667
- usage: usage10,
3668
- run: run10
4461
+ usage: usage11,
4462
+ run: run11
3669
4463
  };
3670
4464
 
3671
4465
  // src/commands/patch.ts
3672
4466
  import { spawnSync as spawnSync3 } from "child_process";
3673
- import { parseArgs as parseArgs12 } from "util";
3674
- var usage11 = `hogsend patch <package> [--cwd <dir>]
4467
+ import { parseArgs as parseArgs13 } from "util";
4468
+ var usage12 = `hogsend patch <package> [--cwd <dir>]
3675
4469
 
3676
4470
  Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
3677
4471
  extracts the package into a temp dir and prints the path to edit. After editing,
@@ -3682,8 +4476,8 @@ This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
3682
4476
  Options:
3683
4477
  --cwd <dir> Project root to run pnpm in (defaults to current directory).
3684
4478
  -h, --help Show this help.`;
3685
- async function run11(ctx) {
3686
- const { values: values2, positionals } = parseArgs12({
4479
+ async function run12(ctx) {
4480
+ const { values: values2, positionals } = parseArgs13({
3687
4481
  args: ctx.argv,
3688
4482
  allowPositionals: true,
3689
4483
  options: {
@@ -3692,7 +4486,7 @@ async function run11(ctx) {
3692
4486
  }
3693
4487
  });
3694
4488
  if (values2.help) {
3695
- ctx.out.log(usage11);
4489
+ ctx.out.log(usage12);
3696
4490
  return;
3697
4491
  }
3698
4492
  const pkg = positionals[0];
@@ -3732,16 +4526,16 @@ async function run11(ctx) {
3732
4526
  var patchCommand = {
3733
4527
  name: "patch",
3734
4528
  summary: "Patch a package via pnpm's native patch flow",
3735
- usage: usage11,
3736
- run: run11
4529
+ usage: usage12,
4530
+ run: run12
3737
4531
  };
3738
4532
 
3739
4533
  // src/commands/setup.ts
3740
4534
  import { existsSync as existsSync7 } from "fs";
3741
4535
  import { join as join7 } from "path";
3742
- import { parseArgs as parseArgs13 } from "util";
3743
- import { confirm as confirm2 } from "@clack/prompts";
3744
- 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]
3745
4539
 
3746
4540
  Interactive local onboarding for a scaffolded Hogsend app. Mirrors the
3747
4541
  create-hogsend "next steps":
@@ -3758,8 +4552,8 @@ Options:
3758
4552
  -h, --help Show this help.
3759
4553
 
3760
4554
  Run ${color.cyan("hogsend doctor")} afterwards to verify the instance is healthy.`;
3761
- async function run12(ctx) {
3762
- const { values: values2 } = parseArgs13({
4555
+ async function run13(ctx) {
4556
+ const { values: values2 } = parseArgs14({
3763
4557
  args: ctx.argv,
3764
4558
  allowPositionals: true,
3765
4559
  options: {
@@ -3769,7 +4563,7 @@ async function run12(ctx) {
3769
4563
  }
3770
4564
  });
3771
4565
  if (values2.help) {
3772
- ctx.out.log(usage12);
4566
+ ctx.out.log(usage13);
3773
4567
  return;
3774
4568
  }
3775
4569
  const cwd = values2.cwd ?? process.cwd();
@@ -3787,7 +4581,7 @@ async function run12(ctx) {
3787
4581
  }
3788
4582
  if (ctx.out.interactive && !skipConfirm) {
3789
4583
  const proceed = bail(
3790
- await confirm2({
4584
+ await confirm3({
3791
4585
  message: `Set up local infra in ${color.cyan(cwd)}? (docker compose up, .env, db:migrate)`
3792
4586
  })
3793
4587
  );
@@ -3884,16 +4678,16 @@ async function run12(ctx) {
3884
4678
  var setupCommand = {
3885
4679
  name: "setup",
3886
4680
  summary: "Local onboarding: docker compose up, gen secret, db:migrate",
3887
- usage: usage12,
3888
- run: run12
4681
+ usage: usage13,
4682
+ run: run13
3889
4683
  };
3890
4684
 
3891
4685
  // src/commands/skills.ts
3892
4686
  import { existsSync as existsSync8 } from "fs";
3893
4687
  import { join as join8 } from "path";
3894
- import { parseArgs as parseArgs14 } from "util";
4688
+ import { parseArgs as parseArgs15 } from "util";
3895
4689
  import { multiselect } from "@clack/prompts";
3896
- var usage13 = `hogsend skills <subcommand> [options]
4690
+ var usage14 = `hogsend skills <subcommand> [options]
3897
4691
 
3898
4692
  Manage the Claude Code skills bundled with @hogsend/cli. Bundled skills teach
3899
4693
  agents how to drive the hogsend CLI; \`add\` copies them into your project's
@@ -3954,7 +4748,7 @@ function runList3(ctx) {
3954
4748
  );
3955
4749
  }
3956
4750
  async function runAdd2(ctx, argv) {
3957
- const { values: values2, positionals } = parseArgs14({
4751
+ const { values: values2, positionals } = parseArgs15({
3958
4752
  args: argv,
3959
4753
  allowPositionals: true,
3960
4754
  options: {
@@ -3964,7 +4758,7 @@ async function runAdd2(ctx, argv) {
3964
4758
  }
3965
4759
  });
3966
4760
  if (values2.help) {
3967
- ctx.out.log(usage13);
4761
+ ctx.out.log(usage14);
3968
4762
  return;
3969
4763
  }
3970
4764
  const cwd = process.cwd();
@@ -4032,7 +4826,7 @@ async function runAdd2(ctx, argv) {
4032
4826
  `Installed ${installedCount} skill${installedCount === 1 ? "" : "s"}` + (skippedCount > 0 ? `, skipped ${skippedCount}.` : ".")
4033
4827
  );
4034
4828
  }
4035
- async function run13(ctx) {
4829
+ async function run14(ctx) {
4036
4830
  const sub = ctx.argv[0];
4037
4831
  switch (sub) {
4038
4832
  case "list":
@@ -4044,7 +4838,7 @@ async function run13(ctx) {
4044
4838
  case void 0:
4045
4839
  case "-h":
4046
4840
  case "--help":
4047
- ctx.out.log(usage13);
4841
+ ctx.out.log(usage14);
4048
4842
  return;
4049
4843
  default:
4050
4844
  ctx.out.fail(
@@ -4055,13 +4849,13 @@ async function run13(ctx) {
4055
4849
  var skillsCommand = {
4056
4850
  name: "skills",
4057
4851
  summary: "List + install bundled Claude Code skills into .claude/skills",
4058
- usage: usage13,
4059
- run: run13
4852
+ usage: usage14,
4853
+ run: run14
4060
4854
  };
4061
4855
 
4062
4856
  // src/commands/stats.ts
4063
- import { parseArgs as parseArgs15 } from "util";
4064
- var usage14 = `hogsend stats [--json]
4857
+ import { parseArgs as parseArgs16 } from "util";
4858
+ var usage15 = `hogsend stats [--json]
4065
4859
 
4066
4860
  Show system-wide overview metrics from a running Hogsend instance.
4067
4861
  Wraps GET /v1/admin/metrics/overview.
@@ -4083,8 +4877,8 @@ Options:
4083
4877
  function pct(rate) {
4084
4878
  return `${(rate * 100).toFixed(2)}%`;
4085
4879
  }
4086
- async function run14(ctx) {
4087
- const { values: values2 } = parseArgs15({
4880
+ async function run15(ctx) {
4881
+ const { values: values2 } = parseArgs16({
4088
4882
  args: ctx.argv,
4089
4883
  allowPositionals: true,
4090
4884
  options: {
@@ -4092,7 +4886,7 @@ async function run14(ctx) {
4092
4886
  }
4093
4887
  });
4094
4888
  if (values2.help) {
4095
- ctx.out.log(usage14);
4889
+ ctx.out.log(usage15);
4096
4890
  return;
4097
4891
  }
4098
4892
  const metrics = await ctx.out.step(
@@ -4121,20 +4915,19 @@ async function run14(ctx) {
4121
4915
  var statsCommand = {
4122
4916
  name: "stats",
4123
4917
  summary: "Show system-wide overview metrics",
4124
- usage: usage14,
4125
- run: run14
4918
+ usage: usage15,
4919
+ run: run15
4126
4920
  };
4127
4921
 
4128
4922
  // src/commands/studio.ts
4129
- import { spawn as spawn2 } from "child_process";
4130
4923
  import { createReadStream, existsSync as existsSync9, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
4131
- import { createServer } from "http";
4924
+ import { createServer as createServer2 } from "http";
4132
4925
  import { extname, join as join9, normalize, resolve as resolve2, sep as sep3 } from "path";
4133
4926
  import { fileURLToPath as fileURLToPath2 } from "url";
4134
- import { parseArgs as parseArgs17 } from "util";
4927
+ import { parseArgs as parseArgs18 } from "util";
4135
4928
 
4136
4929
  // src/commands/studio-admin.ts
4137
- import { parseArgs as parseArgs16 } from "util";
4930
+ import { parseArgs as parseArgs17 } from "util";
4138
4931
  import { password as passwordPrompt, text as text2 } from "@clack/prompts";
4139
4932
 
4140
4933
  // ../../node_modules/.pnpm/postgres@3.4.9/node_modules/postgres/src/index.js
@@ -13322,6 +14115,7 @@ __export(schema_exports, {
13322
14115
  memberRelations: () => memberRelations,
13323
14116
  organization: () => organization,
13324
14117
  organizationRelations: () => organizationRelations,
14118
+ providerCredentials: () => providerCredentials,
13325
14119
  session: () => session,
13326
14120
  sessionRelations: () => sessionRelations,
13327
14121
  trackedLinks: () => trackedLinks,
@@ -14024,6 +14818,30 @@ var linkClicks = pgTable(
14024
14818
  ]
14025
14819
  );
14026
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
+
14027
14845
  // ../db/src/schema/user-events.ts
14028
14846
  var userEvents = pgTable(
14029
14847
  "user_events",
@@ -14519,7 +15337,7 @@ Examples:
14519
15337
  Security: passwords are written ONLY via better-auth (scrypt) \u2014 never raw SQL,
14520
15338
  never plaintext at rest, never logged. Prefer the masked prompt over --password.`;
14521
15339
  function parseAdminFlags(argv) {
14522
- const { values: values2 } = parseArgs16({
15340
+ const { values: values2 } = parseArgs17({
14523
15341
  args: argv,
14524
15342
  allowPositionals: true,
14525
15343
  strict: false,
@@ -14720,7 +15538,7 @@ async function runStudioAdmin(ctx, argv) {
14720
15538
  }
14721
15539
 
14722
15540
  // src/commands/studio.ts
14723
- var usage15 = `hogsend studio [options]
15541
+ var usage16 = `hogsend studio [options]
14724
15542
 
14725
15543
  Subcommands:
14726
15544
  admin create | reset | list Shell-gated Studio admin recovery (DB + secret).
@@ -14798,24 +15616,12 @@ function indexHtml(distPath, baseUrl) {
14798
15616
  }
14799
15617
  return `${inject}${raw}`;
14800
15618
  }
14801
- function openBrowser(url) {
14802
- const platform = process.platform;
14803
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
14804
- const args = platform === "win32" ? ["/c", "start", "", url] : [url];
14805
- try {
14806
- const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
14807
- child.on("error", () => {
14808
- });
14809
- child.unref();
14810
- } catch {
14811
- }
14812
- }
14813
- async function run15(ctx) {
15619
+ async function run16(ctx) {
14814
15620
  if (ctx.argv[0] === "admin") {
14815
15621
  await runStudioAdmin(ctx, ctx.argv.slice(1));
14816
15622
  return;
14817
15623
  }
14818
- const { values: values2, positionals } = parseArgs17({
15624
+ const { values: values2, positionals } = parseArgs18({
14819
15625
  args: ctx.argv,
14820
15626
  allowPositionals: true,
14821
15627
  strict: false,
@@ -14828,7 +15634,7 @@ async function run15(ctx) {
14828
15634
  }
14829
15635
  });
14830
15636
  if (values2.help) {
14831
- ctx.out.log(usage15);
15637
+ ctx.out.log(usage16);
14832
15638
  return;
14833
15639
  }
14834
15640
  const port = Number(values2.port ?? "3333");
@@ -14846,7 +15652,7 @@ async function run15(ctx) {
14846
15652
  }
14847
15653
  const cleanBase = baseUrl ? baseUrl.replace(/\/+$/, "") : void 0;
14848
15654
  const index2 = indexHtml(distPath, cleanBase);
14849
- const server = createServer((req, res) => {
15655
+ const server = createServer2((req, res) => {
14850
15656
  const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
14851
15657
  const rel = urlPath.replace(/^\/studio/, "");
14852
15658
  if (rel === "" || rel === "/") {
@@ -14914,17 +15720,17 @@ async function run15(ctx) {
14914
15720
  var studioCommand = {
14915
15721
  name: "studio",
14916
15722
  summary: "Serve the bundled Hogsend Studio admin SPA locally",
14917
- usage: usage15,
14918
- run: run15
15723
+ usage: usage16,
15724
+ run: run16
14919
15725
  };
14920
15726
 
14921
15727
  // src/commands/upgrade.ts
14922
15728
  import { spawnSync as spawnSync4 } from "child_process";
14923
15729
  import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
14924
15730
  import { join as join10 } from "path";
14925
- import { parseArgs as parseArgs18 } from "util";
14926
- import { confirm as confirm3 } from "@clack/prompts";
14927
- 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]
14928
15734
 
14929
15735
  Upgrade a scaffolded Hogsend app in one step:
14930
15736
  1. bump every @hogsend/* dependency to latest (or --to <version>), then
@@ -14960,8 +15766,8 @@ function hogsendDeps(cwd) {
14960
15766
  function addArgs(pm, specs) {
14961
15767
  return [pm === "npm" ? "install" : "add", ...specs];
14962
15768
  }
14963
- async function run16(ctx) {
14964
- const { values: values2 } = parseArgs18({
15769
+ async function run17(ctx) {
15770
+ const { values: values2 } = parseArgs19({
14965
15771
  args: ctx.argv,
14966
15772
  allowPositionals: true,
14967
15773
  options: {
@@ -14975,7 +15781,7 @@ async function run16(ctx) {
14975
15781
  }
14976
15782
  });
14977
15783
  if (values2.help) {
14978
- ctx.out.log(usage16);
15784
+ ctx.out.log(usage17);
14979
15785
  return;
14980
15786
  }
14981
15787
  if (values2["deps-only"] && values2["skills-only"]) {
@@ -15019,7 +15825,7 @@ async function run16(ctx) {
15019
15825
  doSkills ? "refresh .claude/skills" : null
15020
15826
  ].filter(Boolean).join(" + ");
15021
15827
  const proceed = bail(
15022
- await confirm3({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
15828
+ await confirm4({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
15023
15829
  );
15024
15830
  if (!proceed) {
15025
15831
  ctx.out.outro(color.dim("Nothing changed."));
@@ -15100,12 +15906,12 @@ async function run16(ctx) {
15100
15906
  var upgradeCommand = {
15101
15907
  name: "upgrade",
15102
15908
  summary: "Bump @hogsend/* deps to latest + refresh vendored skills",
15103
- usage: usage16,
15104
- run: run16
15909
+ usage: usage17,
15910
+ run: run17
15105
15911
  };
15106
15912
 
15107
15913
  // src/commands/webhooks.ts
15108
- import { parseArgs as parseArgs19 } from "util";
15914
+ import { parseArgs as parseArgs20 } from "util";
15109
15915
  var WEBHOOK_EVENT_TYPES = [
15110
15916
  "contact.created",
15111
15917
  "contact.updated",
@@ -15122,7 +15928,7 @@ var WEBHOOK_EVENT_TYPES = [
15122
15928
  "bucket.entered",
15123
15929
  "bucket.left"
15124
15930
  ];
15125
- var usage17 = `hogsend webhooks <subcommand> [options]
15931
+ var usage18 = `hogsend webhooks <subcommand> [options]
15126
15932
 
15127
15933
  Manage outbound webhook endpoints \u2014 the Svix-style signed event stream Hogsend
15128
15934
  emits to your URLs. Wraps the admin routes (/v1/admin/webhooks), so this command
@@ -15212,7 +16018,7 @@ ${color.bold(secret)}`,
15212
16018
  );
15213
16019
  }
15214
16020
  async function runList5(ctx, argv) {
15215
- const { values: values2 } = parseArgs19({
16021
+ const { values: values2 } = parseArgs20({
15216
16022
  args: argv,
15217
16023
  allowPositionals: true,
15218
16024
  options: {
@@ -15223,7 +16029,7 @@ async function runList5(ctx, argv) {
15223
16029
  }
15224
16030
  });
15225
16031
  if (values2.help) {
15226
- ctx.out.log(usage17);
16032
+ ctx.out.log(usage18);
15227
16033
  return;
15228
16034
  }
15229
16035
  const query = {
@@ -15272,13 +16078,13 @@ function renderEndpoint(ctx, ep, title) {
15272
16078
  );
15273
16079
  }
15274
16080
  async function runGet3(ctx, argv) {
15275
- const { values: values2, positionals } = parseArgs19({
16081
+ const { values: values2, positionals } = parseArgs20({
15276
16082
  args: argv,
15277
16083
  allowPositionals: true,
15278
16084
  options: { help: { type: "boolean", short: "h", default: false } }
15279
16085
  });
15280
16086
  if (values2.help) {
15281
- ctx.out.log(usage17);
16087
+ ctx.out.log(usage18);
15282
16088
  return;
15283
16089
  }
15284
16090
  const id = positionals[0];
@@ -15303,7 +16109,7 @@ async function runGet3(ctx, argv) {
15303
16109
  ctx.out.outro(`${res.url} \u2192 ${res.status}`);
15304
16110
  }
15305
16111
  async function runCreate2(ctx, argv) {
15306
- const { values: values2 } = parseArgs19({
16112
+ const { values: values2 } = parseArgs20({
15307
16113
  args: argv,
15308
16114
  allowPositionals: true,
15309
16115
  options: {
@@ -15316,7 +16122,7 @@ async function runCreate2(ctx, argv) {
15316
16122
  }
15317
16123
  });
15318
16124
  if (values2.help) {
15319
- ctx.out.log(usage17);
16125
+ ctx.out.log(usage18);
15320
16126
  return;
15321
16127
  }
15322
16128
  const url = values2.url;
@@ -15350,7 +16156,7 @@ async function runCreate2(ctx, argv) {
15350
16156
  ctx.out.outro(`${color.green("Created")} ${res.id} \u2192 ${res.url}`);
15351
16157
  }
15352
16158
  async function runUpdate(ctx, argv) {
15353
- const { values: values2, positionals } = parseArgs19({
16159
+ const { values: values2, positionals } = parseArgs20({
15354
16160
  args: argv,
15355
16161
  allowPositionals: true,
15356
16162
  options: {
@@ -15364,7 +16170,7 @@ async function runUpdate(ctx, argv) {
15364
16170
  }
15365
16171
  });
15366
16172
  if (values2.help) {
15367
- ctx.out.log(usage17);
16173
+ ctx.out.log(usage18);
15368
16174
  return;
15369
16175
  }
15370
16176
  const id = positionals[0];
@@ -15405,13 +16211,13 @@ async function runUpdate(ctx, argv) {
15405
16211
  ctx.out.outro(`${color.green("Updated")} ${res.id} \u2192 ${res.status}`);
15406
16212
  }
15407
16213
  async function runDelete(ctx, argv) {
15408
- const { values: values2, positionals } = parseArgs19({
16214
+ const { values: values2, positionals } = parseArgs20({
15409
16215
  args: argv,
15410
16216
  allowPositionals: true,
15411
16217
  options: { help: { type: "boolean", short: "h", default: false } }
15412
16218
  });
15413
16219
  if (values2.help) {
15414
- ctx.out.log(usage17);
16220
+ ctx.out.log(usage18);
15415
16221
  return;
15416
16222
  }
15417
16223
  const id = positionals[0];
@@ -15435,13 +16241,13 @@ async function runDelete(ctx, argv) {
15435
16241
  ctx.out.outro(`${color.green("Deleted")} ${id}`);
15436
16242
  }
15437
16243
  async function runRotate(ctx, argv) {
15438
- const { values: values2, positionals } = parseArgs19({
16244
+ const { values: values2, positionals } = parseArgs20({
15439
16245
  args: argv,
15440
16246
  allowPositionals: true,
15441
16247
  options: { help: { type: "boolean", short: "h", default: false } }
15442
16248
  });
15443
16249
  if (values2.help) {
15444
- ctx.out.log(usage17);
16250
+ ctx.out.log(usage18);
15445
16251
  return;
15446
16252
  }
15447
16253
  const id = positionals[0];
@@ -15470,13 +16276,13 @@ async function runRotate(ctx, argv) {
15470
16276
  );
15471
16277
  }
15472
16278
  async function runTest(ctx, argv) {
15473
- const { values: values2, positionals } = parseArgs19({
16279
+ const { values: values2, positionals } = parseArgs20({
15474
16280
  args: argv,
15475
16281
  allowPositionals: true,
15476
16282
  options: { help: { type: "boolean", short: "h", default: false } }
15477
16283
  });
15478
16284
  if (values2.help) {
15479
- ctx.out.log(usage17);
16285
+ ctx.out.log(usage18);
15480
16286
  return;
15481
16287
  }
15482
16288
  const id = positionals[0];
@@ -15502,7 +16308,7 @@ async function runTest(ctx, argv) {
15502
16308
  `${color.green("Enqueued")} a ${color.cyan(res.eventType)} delivery to ${id}.`
15503
16309
  );
15504
16310
  }
15505
- async function run17(ctx) {
16311
+ async function run18(ctx) {
15506
16312
  const sub = ctx.argv[0];
15507
16313
  switch (sub) {
15508
16314
  case "list":
@@ -15533,8 +16339,8 @@ async function run17(ctx) {
15533
16339
  var webhooksCommand = {
15534
16340
  name: "webhooks",
15535
16341
  summary: "Manage outbound webhook endpoints (create, rotate, test)",
15536
- usage: usage17,
15537
- run: run17
16342
+ usage: usage18,
16343
+ run: run18
15538
16344
  };
15539
16345
 
15540
16346
  // src/commands/index.ts
@@ -15548,6 +16354,7 @@ var commands = [
15548
16354
  campaignsCommand,
15549
16355
  webhooksCommand,
15550
16356
  domainCommand,
16357
+ connectCommand,
15551
16358
  hatchetCommand,
15552
16359
  studioCommand,
15553
16360
  devCommand,