@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 +994 -187
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
- package/skills/hogsend-authoring-journeys/references/journey-context.md +4 -1
- package/skills/hogsend-deploy/references/env-and-secrets.md +24 -1
- package/src/__tests__/connect-command.test.ts +104 -0
- package/src/__tests__/connect-flow.test.ts +492 -0
- package/src/__tests__/dev.test.ts +1 -0
- package/src/__tests__/domain-command.test.ts +1 -0
- package/src/__tests__/loopback.test.ts +159 -0
- package/src/__tests__/oauth.test.ts +230 -0
- package/src/commands/connect.ts +149 -0
- package/src/commands/doctor.ts +30 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/studio.ts +1 -16
- package/src/lib/browser.ts +17 -0
- package/src/lib/connect-flow.ts +597 -0
- package/src/lib/http.ts +6 -0
- package/src/lib/loopback.ts +223 -0
- package/src/lib/oauth.ts +256 -0
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
|
|
471
|
-
var
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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
|
|
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:
|
|
819
|
-
run:
|
|
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
|
|
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 =
|
|
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
|
|
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 } =
|
|
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
|
|
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
|
|
1219
|
-
var
|
|
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
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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:
|
|
1498
|
-
run:
|
|
2281
|
+
usage: usage4,
|
|
2282
|
+
run: run4
|
|
1499
2283
|
};
|
|
1500
2284
|
|
|
1501
2285
|
// src/commands/dev.ts
|
|
1502
|
-
var
|
|
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(
|
|
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
|
|
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 } =
|
|
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(
|
|
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:
|
|
1821
|
-
run:
|
|
2604
|
+
usage: usage5,
|
|
2605
|
+
run: run5
|
|
1822
2606
|
};
|
|
1823
2607
|
|
|
1824
2608
|
// src/commands/doctor.ts
|
|
1825
|
-
import { parseArgs as
|
|
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
|
-
|
|
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
|
|
2000
|
-
const { values: values2 } =
|
|
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(
|
|
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:
|
|
2101
|
-
run:
|
|
2904
|
+
usage: usage6,
|
|
2905
|
+
run: run6
|
|
2102
2906
|
};
|
|
2103
2907
|
|
|
2104
2908
|
// src/commands/domain.ts
|
|
2105
|
-
import { parseArgs as
|
|
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
|
|
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 } =
|
|
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(
|
|
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
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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
|
|
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(
|
|
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:
|
|
2739
|
-
run:
|
|
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
|
|
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
|
|
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
|
|
2889
|
-
const { values: values2, positionals } =
|
|
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(
|
|
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:
|
|
2944
|
-
run:
|
|
3737
|
+
usage: usage8,
|
|
3738
|
+
run: run8
|
|
2945
3739
|
};
|
|
2946
3740
|
|
|
2947
3741
|
// src/commands/emails.ts
|
|
2948
|
-
import { parseArgs as
|
|
2949
|
-
var
|
|
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 } =
|
|
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(
|
|
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
|
|
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:
|
|
3119
|
-
run:
|
|
3912
|
+
usage: usage9,
|
|
3913
|
+
run: run9
|
|
3120
3914
|
};
|
|
3121
3915
|
|
|
3122
3916
|
// src/commands/hatchet.ts
|
|
3123
|
-
import { parseArgs as
|
|
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
|
|
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 } =
|
|
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
|
|
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(
|
|
4196
|
+
ctx.out.log(usage10);
|
|
3403
4197
|
return;
|
|
3404
4198
|
}
|
|
3405
4199
|
if (rest.includes("-h") || rest.includes("--help")) {
|
|
3406
|
-
ctx.out.log(
|
|
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:
|
|
3420
|
-
run:
|
|
4213
|
+
usage: usage10,
|
|
4214
|
+
run: run10
|
|
3421
4215
|
};
|
|
3422
4216
|
|
|
3423
4217
|
// src/commands/journeys.ts
|
|
3424
|
-
import { parseArgs as
|
|
3425
|
-
var
|
|
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 } =
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
3668
|
-
run:
|
|
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
|
|
3674
|
-
var
|
|
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
|
|
3686
|
-
const { values: values2, positionals } =
|
|
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(
|
|
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:
|
|
3736
|
-
run:
|
|
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
|
|
3743
|
-
import { confirm as
|
|
3744
|
-
var
|
|
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
|
|
3762
|
-
const { values: values2 } =
|
|
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(
|
|
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
|
|
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:
|
|
3888
|
-
run:
|
|
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
|
|
4688
|
+
import { parseArgs as parseArgs15 } from "util";
|
|
3895
4689
|
import { multiselect } from "@clack/prompts";
|
|
3896
|
-
var
|
|
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 } =
|
|
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(
|
|
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
|
|
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(
|
|
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:
|
|
4059
|
-
run:
|
|
4852
|
+
usage: usage14,
|
|
4853
|
+
run: run14
|
|
4060
4854
|
};
|
|
4061
4855
|
|
|
4062
4856
|
// src/commands/stats.ts
|
|
4063
|
-
import { parseArgs as
|
|
4064
|
-
var
|
|
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
|
|
4087
|
-
const { values: values2 } =
|
|
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(
|
|
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:
|
|
4125
|
-
run:
|
|
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
|
|
4927
|
+
import { parseArgs as parseArgs18 } from "util";
|
|
4135
4928
|
|
|
4136
4929
|
// src/commands/studio-admin.ts
|
|
4137
|
-
import { parseArgs as
|
|
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 } =
|
|
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
|
|
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
|
|
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 } =
|
|
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(
|
|
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 =
|
|
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:
|
|
14918
|
-
run:
|
|
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
|
|
14926
|
-
import { confirm as
|
|
14927
|
-
var
|
|
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
|
|
14964
|
-
const { values: values2 } =
|
|
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(
|
|
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
|
|
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:
|
|
15104
|
-
run:
|
|
15909
|
+
usage: usage17,
|
|
15910
|
+
run: run17
|
|
15105
15911
|
};
|
|
15106
15912
|
|
|
15107
15913
|
// src/commands/webhooks.ts
|
|
15108
|
-
import { parseArgs as
|
|
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
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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 } =
|
|
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(
|
|
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
|
|
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:
|
|
15537
|
-
run:
|
|
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,
|