@hogsend/cli 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1985 -807
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
- package/skills/hogsend-integrate/SKILL.md +198 -0
- package/skills/hogsend-integrate/references/auth-billing-seams.md +199 -0
- package/skills/hogsend-integrate/references/framework-recipes.md +208 -0
- package/skills/hogsend-integrate/references/verification.md +86 -0
- package/skills/hogsend-migrate/SKILL.md +147 -0
- package/skills/hogsend-migrate/references/customerio-mapping.md +93 -0
- package/skills/hogsend-migrate/references/cutover-checklist.md +136 -0
- package/skills/hogsend-migrate/references/loops-mapping.md +132 -0
- package/skills/hogsend-migrate/references/resend-broadcasts-mapping.md +120 -0
- package/src/__tests__/dev.test.ts +323 -0
- package/src/__tests__/dns-apply.test.ts +297 -0
- package/src/__tests__/dns.test.ts +143 -0
- package/src/__tests__/domain-command.test.ts +216 -0
- package/src/__tests__/proc.test.ts +177 -0
- package/src/__tests__/setup-steps.test.ts +363 -0
- package/src/commands/dev.ts +444 -0
- package/src/commands/domain.ts +437 -0
- package/src/commands/events.ts +4 -1
- package/src/commands/index.ts +4 -0
- package/src/commands/setup.ts +34 -163
- package/src/lib/dns-apply.ts +218 -0
- package/src/lib/dns.ts +217 -0
- package/src/lib/proc.ts +189 -0
- package/src/lib/setup-steps.ts +333 -0
- package/studio/assets/index-CSXAjTbe.js +265 -0
- package/studio/assets/index-DCsT0fnT.css +1 -0
- package/studio/index.html +2 -2
- package/studio/assets/index-BBOTQnww.js +0 -250
- package/studio/assets/index-DnfpcXbb.css +0 -1
package/dist/bin.js
CHANGED
|
@@ -819,30 +819,1031 @@ var contactsCommand = {
|
|
|
819
819
|
run: run2
|
|
820
820
|
};
|
|
821
821
|
|
|
822
|
+
// src/commands/dev.ts
|
|
823
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
824
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
825
|
+
import { join as join3, resolve } from "path";
|
|
826
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
827
|
+
|
|
828
|
+
// src/lib/proc.ts
|
|
829
|
+
import { spawn } from "child_process";
|
|
830
|
+
import { createInterface } from "readline";
|
|
831
|
+
function spawnManaged(opts) {
|
|
832
|
+
const child = spawn(opts.cmd, opts.args, {
|
|
833
|
+
cwd: opts.cwd,
|
|
834
|
+
env: { ...process.env, FORCE_COLOR: "1", ...opts.env },
|
|
835
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
836
|
+
detached: process.platform !== "win32"
|
|
837
|
+
});
|
|
838
|
+
const prefix = opts.prefixColor(`[${opts.name}]`);
|
|
839
|
+
const writeOut = opts.sink ?? ((line2) => process.stdout.write(line2));
|
|
840
|
+
const writeErr = opts.sink ?? ((line2) => process.stderr.write(line2));
|
|
841
|
+
if (child.stdout) {
|
|
842
|
+
const rl = createInterface({ input: child.stdout });
|
|
843
|
+
rl.on("line", (line2) => writeOut(`${prefix} ${line2}
|
|
844
|
+
`));
|
|
845
|
+
}
|
|
846
|
+
if (child.stderr) {
|
|
847
|
+
const rl = createInterface({ input: child.stderr });
|
|
848
|
+
rl.on("line", (line2) => writeErr(`${prefix} ${line2}
|
|
849
|
+
`));
|
|
850
|
+
}
|
|
851
|
+
const callbacks = [];
|
|
852
|
+
let exitInfo = null;
|
|
853
|
+
const exited = new Promise((resolve3) => {
|
|
854
|
+
const settle = (info) => {
|
|
855
|
+
if (exitInfo) return;
|
|
856
|
+
exitInfo = info;
|
|
857
|
+
resolve3(info);
|
|
858
|
+
for (const cb of callbacks) cb(info);
|
|
859
|
+
};
|
|
860
|
+
child.once("close", (code, signal) => settle({ code, signal }));
|
|
861
|
+
child.once("error", (err) => {
|
|
862
|
+
writeErr(`${prefix} failed to start: ${err.message}
|
|
863
|
+
`);
|
|
864
|
+
settle({ code: null, signal: null });
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
return {
|
|
868
|
+
name: opts.name,
|
|
869
|
+
child,
|
|
870
|
+
exited,
|
|
871
|
+
onExit(cb) {
|
|
872
|
+
if (exitInfo) {
|
|
873
|
+
cb(exitInfo);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
callbacks.push(cb);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function hasExited(proc) {
|
|
881
|
+
return proc.child.pid === void 0 || proc.child.exitCode !== null || proc.child.signalCode !== null;
|
|
882
|
+
}
|
|
883
|
+
function killTree(proc, signal) {
|
|
884
|
+
const pid = proc.child.pid;
|
|
885
|
+
if (pid === void 0 || hasExited(proc)) return;
|
|
886
|
+
try {
|
|
887
|
+
if (process.platform !== "win32") {
|
|
888
|
+
process.kill(-pid, signal);
|
|
889
|
+
} else {
|
|
890
|
+
proc.child.kill(signal);
|
|
891
|
+
}
|
|
892
|
+
} catch {
|
|
893
|
+
try {
|
|
894
|
+
proc.child.kill(signal);
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function sleep(ms) {
|
|
900
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
901
|
+
}
|
|
902
|
+
async function shutdownAll(procs, opts) {
|
|
903
|
+
if (procs.length === 0) return;
|
|
904
|
+
const timeoutMs = opts?.timeoutMs ?? 5e3;
|
|
905
|
+
for (const proc of procs) killTree(proc, "SIGTERM");
|
|
906
|
+
const allExited = Promise.all(procs.map((p) => p.exited));
|
|
907
|
+
await Promise.race([allExited, sleep(timeoutMs)]);
|
|
908
|
+
const stragglers = procs.filter((p) => !hasExited(p));
|
|
909
|
+
if (stragglers.length === 0) return;
|
|
910
|
+
for (const proc of stragglers) killTree(proc, "SIGKILL");
|
|
911
|
+
await Promise.race([allExited, sleep(2e3)]);
|
|
912
|
+
}
|
|
913
|
+
async function waitForHttp(url, timeoutMs) {
|
|
914
|
+
const deadline = Date.now() + timeoutMs;
|
|
915
|
+
let lastError = "no attempt completed";
|
|
916
|
+
do {
|
|
917
|
+
try {
|
|
918
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(2e3) });
|
|
919
|
+
if (res.ok) return;
|
|
920
|
+
lastError = `last response: HTTP ${res.status}`;
|
|
921
|
+
} catch (err) {
|
|
922
|
+
lastError = `last error: ${err instanceof Error ? err.message : String(err)}`;
|
|
923
|
+
}
|
|
924
|
+
await sleep(500);
|
|
925
|
+
} while (Date.now() < deadline);
|
|
926
|
+
throw new Error(
|
|
927
|
+
`timed out after ${Math.round(timeoutMs / 1e3)}s waiting for ${url} (${lastError})`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/lib/setup-steps.ts
|
|
932
|
+
import { spawnSync } from "child_process";
|
|
933
|
+
import { randomBytes } from "crypto";
|
|
934
|
+
import { copyFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
935
|
+
import { connect } from "net";
|
|
936
|
+
import { join as join2 } from "path";
|
|
937
|
+
|
|
938
|
+
// src/lib/config.ts
|
|
939
|
+
import { existsSync, readFileSync } from "fs";
|
|
940
|
+
import { join } from "path";
|
|
941
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
942
|
+
var DEFAULT_BASE_URL = "http://localhost:3002";
|
|
943
|
+
function parseGlobalFlags(argv) {
|
|
944
|
+
const { values: values2, tokens } = parseArgs3({
|
|
945
|
+
args: argv,
|
|
946
|
+
allowPositionals: true,
|
|
947
|
+
strict: false,
|
|
948
|
+
tokens: true,
|
|
949
|
+
options: {
|
|
950
|
+
url: { type: "string" },
|
|
951
|
+
"admin-key": { type: "string" },
|
|
952
|
+
"data-key": { type: "string" },
|
|
953
|
+
json: { type: "boolean", default: false },
|
|
954
|
+
help: { type: "boolean", short: "h", default: false }
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
const owned = /* @__PURE__ */ new Set(["url", "admin-key", "data-key", "json", "help", "h"]);
|
|
958
|
+
const rest = [];
|
|
959
|
+
for (const token of tokens) {
|
|
960
|
+
if (token.kind === "positional") {
|
|
961
|
+
rest.push(token.value);
|
|
962
|
+
} else if (token.kind === "option") {
|
|
963
|
+
if (owned.has(token.name)) continue;
|
|
964
|
+
rest.push(token.rawName);
|
|
965
|
+
if (token.value !== void 0 && !token.inlineValue) {
|
|
966
|
+
rest.push(token.value);
|
|
967
|
+
} else if (token.inlineValue && token.value !== void 0) {
|
|
968
|
+
rest[rest.length - 1] = `${token.rawName}=${token.value}`;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
url: typeof values2.url === "string" ? values2.url : void 0,
|
|
974
|
+
adminKey: typeof values2["admin-key"] === "string" ? values2["admin-key"] : void 0,
|
|
975
|
+
dataKey: typeof values2["data-key"] === "string" ? values2["data-key"] : void 0,
|
|
976
|
+
json: values2.json === true,
|
|
977
|
+
help: values2.help === true,
|
|
978
|
+
rest
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function loadDotEnv(cwd = process.cwd()) {
|
|
982
|
+
const out = {};
|
|
983
|
+
const file = join(cwd, ".env");
|
|
984
|
+
if (!existsSync(file)) return out;
|
|
985
|
+
let raw;
|
|
986
|
+
try {
|
|
987
|
+
raw = readFileSync(file, "utf8");
|
|
988
|
+
} catch {
|
|
989
|
+
return out;
|
|
990
|
+
}
|
|
991
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
992
|
+
const line2 = rawLine.trim();
|
|
993
|
+
if (line2 === "" || line2.startsWith("#")) continue;
|
|
994
|
+
const withoutExport = line2.startsWith("export ") ? line2.slice("export ".length) : line2;
|
|
995
|
+
const eq2 = withoutExport.indexOf("=");
|
|
996
|
+
if (eq2 === -1) continue;
|
|
997
|
+
const key = withoutExport.slice(0, eq2).trim();
|
|
998
|
+
if (key === "") continue;
|
|
999
|
+
let value = withoutExport.slice(eq2 + 1).trim();
|
|
1000
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1001
|
+
value = value.slice(1, -1);
|
|
1002
|
+
}
|
|
1003
|
+
out[key] = value;
|
|
1004
|
+
}
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
1007
|
+
function resolveConfig(flags, cwd = process.cwd()) {
|
|
1008
|
+
const dotenv = loadDotEnv(cwd);
|
|
1009
|
+
const baseUrlRaw = flags.url ?? process.env.HOGSEND_API_URL ?? dotenv.HOGSEND_API_URL ?? DEFAULT_BASE_URL;
|
|
1010
|
+
const adminKey = flags.adminKey ?? process.env.HOGSEND_ADMIN_KEY ?? process.env.ADMIN_API_KEY ?? dotenv.HOGSEND_ADMIN_KEY ?? dotenv.ADMIN_API_KEY;
|
|
1011
|
+
const dataKey = flags.dataKey ?? process.env.HOGSEND_DATA_KEY ?? process.env.HOGSEND_API_KEY ?? dotenv.HOGSEND_DATA_KEY ?? dotenv.HOGSEND_API_KEY;
|
|
1012
|
+
return {
|
|
1013
|
+
baseUrl: baseUrlRaw.replace(/\/+$/, ""),
|
|
1014
|
+
adminKey: adminKey && adminKey.length > 0 ? adminKey : void 0,
|
|
1015
|
+
dataKey: dataKey && dataKey.length > 0 ? dataKey : void 0
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/lib/setup-steps.ts
|
|
1020
|
+
var SECRET_KEY = "BETTER_AUTH_SECRET";
|
|
1021
|
+
var PLACEHOLDER_PREFIX = "change-me";
|
|
1022
|
+
var PLACEHOLDER_PREFIXES = [PLACEHOLDER_PREFIX, "REPLACE_ME"];
|
|
1023
|
+
function generateSecret() {
|
|
1024
|
+
return randomBytes(32).toString("hex");
|
|
1025
|
+
}
|
|
1026
|
+
var COMPOSE_FILES = [
|
|
1027
|
+
"docker-compose.yml",
|
|
1028
|
+
"docker-compose.yaml",
|
|
1029
|
+
"compose.yml",
|
|
1030
|
+
"compose.yaml"
|
|
1031
|
+
];
|
|
1032
|
+
function hasComposeFile(cwd) {
|
|
1033
|
+
return COMPOSE_FILES.some((name) => existsSync2(join2(cwd, name)));
|
|
1034
|
+
}
|
|
1035
|
+
function readDotEnv(cwd) {
|
|
1036
|
+
return loadDotEnv(cwd);
|
|
1037
|
+
}
|
|
1038
|
+
function ensureEnvFile(cwd) {
|
|
1039
|
+
const envPath = join2(cwd, ".env");
|
|
1040
|
+
const examplePath = join2(cwd, ".env.example");
|
|
1041
|
+
if (existsSync2(envPath)) {
|
|
1042
|
+
return { step: "env", status: "skipped", detail: ".env already exists" };
|
|
1043
|
+
}
|
|
1044
|
+
if (existsSync2(examplePath)) {
|
|
1045
|
+
copyFileSync(examplePath, envPath);
|
|
1046
|
+
return {
|
|
1047
|
+
step: "env",
|
|
1048
|
+
status: "ok",
|
|
1049
|
+
detail: "copied .env.example -> .env"
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
step: "env",
|
|
1054
|
+
status: "failed",
|
|
1055
|
+
detail: "no .env and no .env.example to copy from"
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function ensureAuthSecret(cwd) {
|
|
1059
|
+
const envPath = join2(cwd, ".env");
|
|
1060
|
+
if (!existsSync2(envPath)) {
|
|
1061
|
+
return { step: "secret", status: "skipped", detail: "skipped \u2014 no .env" };
|
|
1062
|
+
}
|
|
1063
|
+
let raw;
|
|
1064
|
+
try {
|
|
1065
|
+
raw = readFileSync2(envPath, "utf8");
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
return {
|
|
1068
|
+
step: "secret",
|
|
1069
|
+
status: "failed",
|
|
1070
|
+
detail: `could not read .env: ${err instanceof Error ? err.message : String(err)}`
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const lines = raw.split(/\r?\n/);
|
|
1074
|
+
const idx = lines.findIndex(
|
|
1075
|
+
(l) => l.replace(/^export\s+/, "").trimStart().startsWith(`${SECRET_KEY}=`)
|
|
1076
|
+
);
|
|
1077
|
+
const existingLine = idx === -1 ? void 0 : lines[idx];
|
|
1078
|
+
const current = existingLine === void 0 ? void 0 : existingLine.slice(existingLine.indexOf("=") + 1).trim();
|
|
1079
|
+
const isPlaceholder = current === void 0 || current === "" || PLACEHOLDER_PREFIXES.some((prefix) => current.startsWith(prefix));
|
|
1080
|
+
if (!isPlaceholder) {
|
|
1081
|
+
return {
|
|
1082
|
+
step: "secret",
|
|
1083
|
+
status: "skipped",
|
|
1084
|
+
detail: `${SECRET_KEY} already set`
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const secret = generateSecret();
|
|
1088
|
+
const newLine = `${SECRET_KEY}=${secret}`;
|
|
1089
|
+
if (idx === -1) {
|
|
1090
|
+
if (raw.length > 0 && !raw.endsWith("\n")) lines.push("");
|
|
1091
|
+
lines.push(newLine);
|
|
1092
|
+
} else {
|
|
1093
|
+
lines[idx] = newLine;
|
|
1094
|
+
}
|
|
1095
|
+
writeFileSync(envPath, lines.join("\n"));
|
|
1096
|
+
return {
|
|
1097
|
+
step: "secret",
|
|
1098
|
+
status: "ok",
|
|
1099
|
+
detail: `generated ${SECRET_KEY} (64-char hex)`
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function runCmd(cmd, args, cwd, quiet) {
|
|
1103
|
+
const result = spawnSync(cmd, args, {
|
|
1104
|
+
cwd,
|
|
1105
|
+
stdio: quiet ? "ignore" : "inherit"
|
|
1106
|
+
});
|
|
1107
|
+
return { status: result.status, ok: result.status === 0 };
|
|
1108
|
+
}
|
|
1109
|
+
async function dockerComposeUp(cwd, opts) {
|
|
1110
|
+
const result = runCmd(
|
|
1111
|
+
"docker",
|
|
1112
|
+
["compose", "up", "-d"],
|
|
1113
|
+
cwd,
|
|
1114
|
+
opts?.quiet ?? false
|
|
1115
|
+
);
|
|
1116
|
+
return {
|
|
1117
|
+
step: "docker",
|
|
1118
|
+
status: result.ok ? "ok" : "failed",
|
|
1119
|
+
detail: result.ok ? "Postgres + Redis + Hatchet-Lite up" : `docker compose exited with code ${result.status ?? "?"}`
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
async function runMigrations(cwd, opts) {
|
|
1123
|
+
const result = runCmd("pnpm", ["db:migrate"], cwd, opts?.quiet ?? false);
|
|
1124
|
+
return {
|
|
1125
|
+
step: "migrate",
|
|
1126
|
+
status: result.ok ? "ok" : "failed",
|
|
1127
|
+
detail: result.ok ? "engine + client migrations applied" : `pnpm db:migrate exited with code ${result.status ?? "?"}`
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function probeTcp(opts) {
|
|
1131
|
+
const { port, host = "127.0.0.1", timeoutMs = 750 } = opts;
|
|
1132
|
+
return new Promise((resolve3) => {
|
|
1133
|
+
let settled = false;
|
|
1134
|
+
const socket = connect({ port, host });
|
|
1135
|
+
const done = (ok) => {
|
|
1136
|
+
if (settled) return;
|
|
1137
|
+
settled = true;
|
|
1138
|
+
socket.destroy();
|
|
1139
|
+
resolve3(ok);
|
|
1140
|
+
};
|
|
1141
|
+
socket.once("connect", () => done(true));
|
|
1142
|
+
socket.once("error", () => done(false));
|
|
1143
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
function envPort(env, key, fallback) {
|
|
1147
|
+
const raw = env[key];
|
|
1148
|
+
if (raw === void 0) return fallback;
|
|
1149
|
+
const n = Number(raw);
|
|
1150
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : fallback;
|
|
1151
|
+
}
|
|
1152
|
+
function parseComposePs(stdout) {
|
|
1153
|
+
const entries = [];
|
|
1154
|
+
const push = (value) => {
|
|
1155
|
+
if (value === null || typeof value !== "object") return;
|
|
1156
|
+
const obj = value;
|
|
1157
|
+
const service = obj.Service ?? obj.Name;
|
|
1158
|
+
const state = obj.State;
|
|
1159
|
+
if (typeof service === "string" && typeof state === "string") {
|
|
1160
|
+
entries.push({ service, state: state.toLowerCase() });
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
const trimmed = stdout.trim();
|
|
1164
|
+
if (trimmed === "") return entries;
|
|
1165
|
+
if (trimmed.startsWith("[")) {
|
|
1166
|
+
try {
|
|
1167
|
+
const arr = JSON.parse(trimmed);
|
|
1168
|
+
if (Array.isArray(arr)) for (const item of arr) push(item);
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1171
|
+
return entries;
|
|
1172
|
+
}
|
|
1173
|
+
for (const line2 of trimmed.split(/\r?\n/)) {
|
|
1174
|
+
try {
|
|
1175
|
+
push(JSON.parse(line2));
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return entries;
|
|
1180
|
+
}
|
|
1181
|
+
async function detectRunningInfra(cwd) {
|
|
1182
|
+
const found = { postgres: false, redis: false, hatchet: false };
|
|
1183
|
+
try {
|
|
1184
|
+
const result = spawnSync("docker", ["compose", "ps", "--format", "json"], {
|
|
1185
|
+
cwd,
|
|
1186
|
+
encoding: "utf8"
|
|
1187
|
+
});
|
|
1188
|
+
if (!result.error && result.status === 0 && typeof result.stdout === "string") {
|
|
1189
|
+
for (const entry of parseComposePs(result.stdout)) {
|
|
1190
|
+
if (entry.state !== "running") continue;
|
|
1191
|
+
if (entry.service === "postgres") {
|
|
1192
|
+
found.postgres = true;
|
|
1193
|
+
} else if (entry.service === "redis") {
|
|
1194
|
+
found.redis = true;
|
|
1195
|
+
} else if (entry.service.startsWith("hatchet") && !entry.service.includes("postgres")) {
|
|
1196
|
+
found.hatchet = true;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
if (found.postgres && found.redis && found.hatchet) return found;
|
|
1203
|
+
try {
|
|
1204
|
+
const env = readDotEnv(cwd);
|
|
1205
|
+
const [postgres2, redis, hatchet] = await Promise.all([
|
|
1206
|
+
found.postgres || probeTcp({ port: envPort(env, "POSTGRES_PORT", 5434) }),
|
|
1207
|
+
found.redis || probeTcp({ port: envPort(env, "REDIS_PORT", 6380) }),
|
|
1208
|
+
found.hatchet || probeTcp({ port: envPort(env, "HATCHET_DASHBOARD_PORT", 8888) })
|
|
1209
|
+
]);
|
|
1210
|
+
return { postgres: postgres2, redis, hatchet };
|
|
1211
|
+
} catch {
|
|
1212
|
+
return found;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/commands/events.ts
|
|
1217
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
1218
|
+
var usage3 = `hogsend events <userId> [options]
|
|
1219
|
+
hogsend events send <name> [options]
|
|
1220
|
+
|
|
1221
|
+
Read a single user's event history (admin API), or send an event into the data
|
|
1222
|
+
plane to drive journeys/buckets.
|
|
1223
|
+
|
|
1224
|
+
Read mode \u2014 hogsend events <userId>:
|
|
1225
|
+
Stream the event history for a single user, newest first. Wraps
|
|
1226
|
+
GET /v1/admin/events?userId=<userId>.
|
|
1227
|
+
|
|
1228
|
+
Arguments:
|
|
1229
|
+
<userId> The user (distinct) id to fetch events for. Required.
|
|
1230
|
+
|
|
1231
|
+
Options:
|
|
1232
|
+
--event <name> Filter to a single event name.
|
|
1233
|
+
--from <iso> Only events at/after this ISO-8601 timestamp.
|
|
1234
|
+
--to <iso> Only events at/before this ISO-8601 timestamp.
|
|
1235
|
+
--limit <n> Max events to return (1-100, default 50).
|
|
1236
|
+
--offset <n> Pagination offset (default 0).
|
|
1237
|
+
|
|
1238
|
+
Send mode \u2014 hogsend events send <name>:
|
|
1239
|
+
Push an event into POST /v1/events (data plane, ingest key). At least one of
|
|
1240
|
+
--email / --user-id is required.
|
|
1241
|
+
|
|
1242
|
+
Options:
|
|
1243
|
+
--email <addr> Recipient/identity email.
|
|
1244
|
+
--user-id <id> External (distinct) id.
|
|
1245
|
+
--prop <key=value> Event property; repeatable. Value parsed as JSON,
|
|
1246
|
+
falling back to a string.
|
|
1247
|
+
--props <json> Event properties as one JSON object.
|
|
1248
|
+
--contact-prop <k=v> Contact property to merge onto the contact; repeatable.
|
|
1249
|
+
--contact-props <json> Contact properties as one JSON object.
|
|
1250
|
+
--list <id> Subscribe to a list; repeatable.
|
|
1251
|
+
--unlist <id> Unsubscribe from a list; repeatable.
|
|
1252
|
+
--idempotency-key <k> Dedup key (sent as the Idempotency-Key header).
|
|
1253
|
+
--timestamp <iso> Override the event timestamp.
|
|
1254
|
+
|
|
1255
|
+
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
1256
|
+
-h/--help.
|
|
1257
|
+
|
|
1258
|
+
Examples:
|
|
1259
|
+
hogsend events user_123
|
|
1260
|
+
hogsend events user_123 --event signup --limit 10
|
|
1261
|
+
hogsend events user_123 --from 2026-01-01T00:00:00Z --json
|
|
1262
|
+
hogsend events send signup --user-id user_123 --prop plan=pro
|
|
1263
|
+
hogsend events send purchase --email a@b.com --props '{"amount":49}' --json`;
|
|
1264
|
+
async function run3(ctx) {
|
|
1265
|
+
if (ctx.argv[0] === "send") {
|
|
1266
|
+
return runSend2(ctx, ctx.argv.slice(1));
|
|
1267
|
+
}
|
|
1268
|
+
return runRead(ctx, ctx.argv);
|
|
1269
|
+
}
|
|
1270
|
+
async function runRead(ctx, argv) {
|
|
1271
|
+
const { values: values2, positionals } = parseArgs4({
|
|
1272
|
+
args: argv,
|
|
1273
|
+
allowPositionals: true,
|
|
1274
|
+
options: {
|
|
1275
|
+
event: { type: "string" },
|
|
1276
|
+
from: { type: "string" },
|
|
1277
|
+
to: { type: "string" },
|
|
1278
|
+
limit: { type: "string" },
|
|
1279
|
+
offset: { type: "string" },
|
|
1280
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
if (values2.help) {
|
|
1284
|
+
ctx.out.log(usage3);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const userId = positionals[0];
|
|
1288
|
+
if (!userId) {
|
|
1289
|
+
ctx.out.fail("events requires a userId, e.g. hogsend events user_123");
|
|
1290
|
+
}
|
|
1291
|
+
const limit = parseNumber(values2.limit, "limit", ctx);
|
|
1292
|
+
const offset = parseNumber(values2.offset, "offset", ctx);
|
|
1293
|
+
const query = {
|
|
1294
|
+
userId,
|
|
1295
|
+
event: values2.event,
|
|
1296
|
+
from: values2.from,
|
|
1297
|
+
to: values2.to,
|
|
1298
|
+
limit,
|
|
1299
|
+
offset
|
|
1300
|
+
};
|
|
1301
|
+
let data;
|
|
1302
|
+
try {
|
|
1303
|
+
data = await ctx.out.step(
|
|
1304
|
+
`Fetching events for ${userId}`,
|
|
1305
|
+
() => ctx.http.get("/v1/admin/events", query)
|
|
1306
|
+
);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
if (isHttpError(error)) {
|
|
1309
|
+
ctx.out.fail(error.message);
|
|
1310
|
+
}
|
|
1311
|
+
throw error;
|
|
1312
|
+
}
|
|
1313
|
+
if (ctx.json) {
|
|
1314
|
+
ctx.out.json(data);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} events`);
|
|
1318
|
+
if (data.events.length === 0) {
|
|
1319
|
+
ctx.out.note(
|
|
1320
|
+
`No events found for ${color.cyan(userId)}.`,
|
|
1321
|
+
"Empty event stream"
|
|
1322
|
+
);
|
|
1323
|
+
ctx.out.outro(color.dim("Nothing to show."));
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const rows = data.events.map((e) => ({
|
|
1327
|
+
occurredAt: e.occurredAt,
|
|
1328
|
+
event: e.event,
|
|
1329
|
+
properties: summarizeProps(e.properties),
|
|
1330
|
+
id: e.id
|
|
1331
|
+
}));
|
|
1332
|
+
ctx.out.table(rows, ["occurredAt", "event", "properties", "id"]);
|
|
1333
|
+
const shown = data.events.length;
|
|
1334
|
+
const through = data.offset + shown;
|
|
1335
|
+
ctx.out.outro(
|
|
1336
|
+
`${color.green(String(shown))} event${shown === 1 ? "" : "s"} ` + color.dim(`(${data.offset + 1}-${through} of ${data.total})`)
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
async function runSend2(ctx, argv) {
|
|
1340
|
+
const { values: values2, positionals } = parseArgs4({
|
|
1341
|
+
args: argv,
|
|
1342
|
+
allowPositionals: true,
|
|
1343
|
+
options: {
|
|
1344
|
+
email: { type: "string" },
|
|
1345
|
+
"user-id": { type: "string" },
|
|
1346
|
+
prop: { type: "string", multiple: true },
|
|
1347
|
+
props: { type: "string" },
|
|
1348
|
+
"contact-prop": { type: "string", multiple: true },
|
|
1349
|
+
"contact-props": { type: "string" },
|
|
1350
|
+
list: { type: "string", multiple: true },
|
|
1351
|
+
unlist: { type: "string", multiple: true },
|
|
1352
|
+
"idempotency-key": { type: "string" },
|
|
1353
|
+
timestamp: { type: "string" },
|
|
1354
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
if (values2.help) {
|
|
1358
|
+
ctx.out.log(usage3);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const name = positionals[0];
|
|
1362
|
+
if (!name) {
|
|
1363
|
+
ctx.out.fail(
|
|
1364
|
+
"events send requires an event name, e.g. hogsend events send signup --user-id user_123"
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
const email = values2.email;
|
|
1368
|
+
const userId = values2["user-id"];
|
|
1369
|
+
if (!email && !userId) {
|
|
1370
|
+
ctx.out.fail("events send requires at least one of --email or --user-id");
|
|
1371
|
+
}
|
|
1372
|
+
const eventProperties = parseProps3(ctx, values2.props, values2.prop, "prop");
|
|
1373
|
+
const contactProperties = parseProps3(
|
|
1374
|
+
ctx,
|
|
1375
|
+
values2["contact-props"],
|
|
1376
|
+
values2["contact-prop"],
|
|
1377
|
+
"contact-prop"
|
|
1378
|
+
);
|
|
1379
|
+
const lists = parseLists2(values2.list, values2.unlist);
|
|
1380
|
+
const body = { name };
|
|
1381
|
+
if (email) body.email = email;
|
|
1382
|
+
if (userId) body.userId = userId;
|
|
1383
|
+
if (eventProperties) body.eventProperties = eventProperties;
|
|
1384
|
+
if (contactProperties) body.contactProperties = contactProperties;
|
|
1385
|
+
if (lists) body.lists = lists;
|
|
1386
|
+
if (values2["idempotency-key"]) {
|
|
1387
|
+
body.idempotencyKey = values2["idempotency-key"];
|
|
1388
|
+
}
|
|
1389
|
+
if (values2.timestamp) body.timestamp = values2.timestamp;
|
|
1390
|
+
let res;
|
|
1391
|
+
try {
|
|
1392
|
+
res = await ctx.out.step(
|
|
1393
|
+
`Sending event ${name}`,
|
|
1394
|
+
() => ctx.dataHttp.post("/v1/events", body)
|
|
1395
|
+
);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
if (isHttpError(error)) {
|
|
1398
|
+
ctx.out.fail(error.message);
|
|
1399
|
+
}
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
if (ctx.json) {
|
|
1403
|
+
ctx.out.json(res);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
ctx.out.intro(`${color.bgMagenta(color.black(" hogsend "))} events send`);
|
|
1407
|
+
const exited = res.exits.filter((e) => e.exited);
|
|
1408
|
+
ctx.out.kv(
|
|
1409
|
+
{
|
|
1410
|
+
event: name,
|
|
1411
|
+
stored: res.stored,
|
|
1412
|
+
identity: email ?? userId ?? "",
|
|
1413
|
+
exits: res.exits.length,
|
|
1414
|
+
"journeys exited": exited.length
|
|
1415
|
+
},
|
|
1416
|
+
"Event sent"
|
|
1417
|
+
);
|
|
1418
|
+
if (exited.length > 0) {
|
|
1419
|
+
ctx.out.table(
|
|
1420
|
+
exited.map((e) => ({ journeyId: e.journeyId, stateId: e.stateId })),
|
|
1421
|
+
["journeyId", "stateId"]
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
ctx.out.outro(
|
|
1425
|
+
res.stored ? `${color.green("Stored")} ${name}.` : color.dim(`${name} was deduped (not stored).`)
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
function parseProps3(ctx, json2, pairs, flagName) {
|
|
1429
|
+
const out = {};
|
|
1430
|
+
let any = false;
|
|
1431
|
+
if (json2 !== void 0) {
|
|
1432
|
+
let parsed;
|
|
1433
|
+
try {
|
|
1434
|
+
parsed = JSON.parse(json2);
|
|
1435
|
+
} catch {
|
|
1436
|
+
ctx.out.fail(`--${flagName}s must be valid JSON, got: ${json2}`);
|
|
1437
|
+
}
|
|
1438
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1439
|
+
ctx.out.fail(`--${flagName}s must be a JSON object`);
|
|
1440
|
+
}
|
|
1441
|
+
Object.assign(out, parsed);
|
|
1442
|
+
any = true;
|
|
1443
|
+
}
|
|
1444
|
+
for (const pair of pairs ?? []) {
|
|
1445
|
+
const eq2 = pair.indexOf("=");
|
|
1446
|
+
if (eq2 === -1) {
|
|
1447
|
+
ctx.out.fail(`--${flagName} must be key=value, got: ${pair}`);
|
|
1448
|
+
}
|
|
1449
|
+
const key = pair.slice(0, eq2).trim();
|
|
1450
|
+
if (key === "") {
|
|
1451
|
+
ctx.out.fail(`--${flagName} key cannot be empty, got: ${pair}`);
|
|
1452
|
+
}
|
|
1453
|
+
out[key] = coerceValue3(pair.slice(eq2 + 1));
|
|
1454
|
+
any = true;
|
|
1455
|
+
}
|
|
1456
|
+
return any ? out : void 0;
|
|
1457
|
+
}
|
|
1458
|
+
function coerceValue3(raw) {
|
|
1459
|
+
try {
|
|
1460
|
+
return JSON.parse(raw);
|
|
1461
|
+
} catch {
|
|
1462
|
+
return raw;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function parseLists2(subscribe, unsubscribe) {
|
|
1466
|
+
const out = {};
|
|
1467
|
+
let any = false;
|
|
1468
|
+
for (const id of subscribe ?? []) {
|
|
1469
|
+
out[id] = true;
|
|
1470
|
+
any = true;
|
|
1471
|
+
}
|
|
1472
|
+
for (const id of unsubscribe ?? []) {
|
|
1473
|
+
out[id] = false;
|
|
1474
|
+
any = true;
|
|
1475
|
+
}
|
|
1476
|
+
return any ? out : void 0;
|
|
1477
|
+
}
|
|
1478
|
+
function parseNumber(raw, name, ctx) {
|
|
1479
|
+
if (raw === void 0) return void 0;
|
|
1480
|
+
const n = Number(raw);
|
|
1481
|
+
if (!Number.isFinite(n)) {
|
|
1482
|
+
ctx.out.fail(`--${name} must be a number, got "${raw}"`);
|
|
1483
|
+
}
|
|
1484
|
+
return n;
|
|
1485
|
+
}
|
|
1486
|
+
function summarizeProps(props) {
|
|
1487
|
+
if (!props) return "";
|
|
1488
|
+
const keys = Object.keys(props);
|
|
1489
|
+
if (keys.length === 0) return "";
|
|
1490
|
+
const preview = JSON.stringify(props);
|
|
1491
|
+
return preview.length > 60 ? `${preview.slice(0, 57)}...` : preview;
|
|
1492
|
+
}
|
|
1493
|
+
var eventsCommand = {
|
|
1494
|
+
name: "events",
|
|
1495
|
+
summary: "Stream a user's event history, or send an event",
|
|
1496
|
+
usage: usage3,
|
|
1497
|
+
run: run3
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
// src/commands/dev.ts
|
|
1501
|
+
var usage4 = `hogsend dev [options]
|
|
1502
|
+
hogsend dev --fire <event> [event-send options]
|
|
1503
|
+
|
|
1504
|
+
Run the full local stack for a Hogsend app from one command:
|
|
1505
|
+
|
|
1506
|
+
1. infra detect running containers, docker compose up -d when needed
|
|
1507
|
+
2. .env cp .env.example -> .env + generate BETTER_AUTH_SECRET
|
|
1508
|
+
3. migrate pnpm db:migrate (when the app has the script)
|
|
1509
|
+
4. spawn [api] pnpm run dev + [worker] hatchet worker dev
|
|
1510
|
+
(falls back to pnpm run worker:dev without hatchet CLI/config)
|
|
1511
|
+
5. health wait for GET /v1/health, then print the local URLs
|
|
1512
|
+
|
|
1513
|
+
Ctrl+C stops everything \u2014 the API, the worker, and their whole process trees.
|
|
1514
|
+
|
|
1515
|
+
Options:
|
|
1516
|
+
--cwd <dir> Project root to run in (defaults to the current directory).
|
|
1517
|
+
--no-worker Start the API only (skip the worker process).
|
|
1518
|
+
--no-infra Skip the docker/.env/migrate steps (infra managed elsewhere).
|
|
1519
|
+
--fire <event> Don't boot anything \u2014 send a test event to the RUNNING
|
|
1520
|
+
instance via POST /v1/events (works from a second terminal
|
|
1521
|
+
while hogsend dev runs in the first). Accepts every
|
|
1522
|
+
\`hogsend events send\` option: --email, --user-id, --prop,
|
|
1523
|
+
--props, --contact-prop, --contact-props, --list, --unlist,
|
|
1524
|
+
--idempotency-key, --timestamp.
|
|
1525
|
+
-h, --help Show this help.
|
|
1526
|
+
|
|
1527
|
+
Examples:
|
|
1528
|
+
hogsend dev
|
|
1529
|
+
hogsend dev --cwd apps/api
|
|
1530
|
+
hogsend dev --no-worker
|
|
1531
|
+
hogsend dev --fire signup --email a@b.com --prop plan=pro`;
|
|
1532
|
+
function renderDomainLine(d) {
|
|
1533
|
+
if (d.testMode?.active) {
|
|
1534
|
+
const target = d.testMode.redirectTo ?? "(no redirect address)";
|
|
1535
|
+
const state = d.status?.state;
|
|
1536
|
+
const suffix = d.domain && state && state !== "verified" ? ` (domain ${state})` : "";
|
|
1537
|
+
return color.yellow(
|
|
1538
|
+
`Test mode active \u2014 emails redirect to ${target}${suffix}`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
if (d.domain && d.status) {
|
|
1542
|
+
const state = d.status.state === "verified" ? color.green("verified") : color.yellow(d.status.state);
|
|
1543
|
+
return `${color.dim("Domain")} ${d.domain} \u2014 ${state}`;
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
async function fetchDomainLine(ctx) {
|
|
1548
|
+
if (!ctx.cfg.adminKey) return null;
|
|
1549
|
+
try {
|
|
1550
|
+
const d = await ctx.http.get("/v1/admin/domain");
|
|
1551
|
+
if (d === null || typeof d !== "object") return null;
|
|
1552
|
+
return renderDomainLine(d);
|
|
1553
|
+
} catch {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function extractFire(argv) {
|
|
1558
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1559
|
+
const token = argv[i];
|
|
1560
|
+
if (token === "--fire") {
|
|
1561
|
+
const next = argv[i + 1];
|
|
1562
|
+
if (next === void 0 || next.startsWith("-")) {
|
|
1563
|
+
return {
|
|
1564
|
+
error: "--fire requires an event name, e.g. hogsend dev --fire signup --email a@b.com"
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
event: next,
|
|
1569
|
+
rest: [...argv.slice(0, i), ...argv.slice(i + 2)]
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
if (token.startsWith("--fire=")) {
|
|
1573
|
+
const event = token.slice("--fire=".length);
|
|
1574
|
+
if (event === "") {
|
|
1575
|
+
return { error: "--fire requires an event name" };
|
|
1576
|
+
}
|
|
1577
|
+
return { event, rest: [...argv.slice(0, i), ...argv.slice(i + 1)] };
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
async function runFire(ctx, event, rest) {
|
|
1583
|
+
if (rest.includes("-h") || rest.includes("--help")) {
|
|
1584
|
+
ctx.out.log(usage4);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
const res = await fetch(`${ctx.cfg.baseUrl}/v1/health`, {
|
|
1589
|
+
signal: AbortSignal.timeout(2e3)
|
|
1590
|
+
});
|
|
1591
|
+
if (!res.ok) throw new Error(`health returned HTTP ${res.status}`);
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1594
|
+
ctx.out.fail(
|
|
1595
|
+
`cannot reach ${ctx.cfg.baseUrl} \u2014 is hogsend dev running? (${msg})`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
await runSend2(ctx, [event, ...rest]);
|
|
1599
|
+
}
|
|
1600
|
+
function assertHogsendApp(cwd, ctx) {
|
|
1601
|
+
const pkgPath = join3(cwd, "package.json");
|
|
1602
|
+
if (!existsSync3(pkgPath)) {
|
|
1603
|
+
ctx.out.fail(
|
|
1604
|
+
`not a Hogsend app \u2014 no package.json in ${cwd}. Run inside a scaffolded app (pnpm dlx create-hogsend@latest) or pass --cwd <dir>.`
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
let pkg;
|
|
1608
|
+
try {
|
|
1609
|
+
pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
1610
|
+
} catch (err) {
|
|
1611
|
+
ctx.out.fail(
|
|
1612
|
+
`could not parse ${pkgPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
const scripts = pkg.scripts ?? {};
|
|
1616
|
+
for (const script of ["dev", "worker:dev"]) {
|
|
1617
|
+
if (!scripts[script]) {
|
|
1618
|
+
ctx.out.fail(
|
|
1619
|
+
`not a runnable Hogsend app \u2014 package.json in ${cwd} has no "${script}" script. Scaffold one with pnpm dlx create-hogsend@latest, or pass --cwd <dir>.`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1624
|
+
if (!deps["@hogsend/engine"]) {
|
|
1625
|
+
ctx.out.fail(
|
|
1626
|
+
`not a Hogsend app \u2014 @hogsend/engine is not a dependency in ${pkgPath}. Scaffold one with pnpm dlx create-hogsend@latest, or pass --cwd <dir>.`
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
return pkg;
|
|
1630
|
+
}
|
|
1631
|
+
function hatchetOnPath() {
|
|
1632
|
+
try {
|
|
1633
|
+
const result = spawnSync2("hatchet", ["--version"], { stdio: "ignore" });
|
|
1634
|
+
return !result.error && result.status === 0;
|
|
1635
|
+
} catch {
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
function parsePort(raw, fallback) {
|
|
1640
|
+
if (raw === void 0) return fallback;
|
|
1641
|
+
const n = Number(raw);
|
|
1642
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : fallback;
|
|
1643
|
+
}
|
|
1644
|
+
async function prepareInfra(ctx, cwd, pkg) {
|
|
1645
|
+
if (!hasComposeFile(cwd)) {
|
|
1646
|
+
ctx.out.log(
|
|
1647
|
+
color.dim(
|
|
1648
|
+
" no docker-compose file \u2014 skipping docker (infra managed elsewhere)"
|
|
1649
|
+
)
|
|
1650
|
+
);
|
|
1651
|
+
} else {
|
|
1652
|
+
const infra = await ctx.out.step(
|
|
1653
|
+
"Checking infra",
|
|
1654
|
+
() => detectRunningInfra(cwd)
|
|
1655
|
+
);
|
|
1656
|
+
if (infra.postgres && infra.redis && infra.hatchet) {
|
|
1657
|
+
ctx.out.log(
|
|
1658
|
+
color.dim(" infra already running \u2014 skipping docker compose up")
|
|
1659
|
+
);
|
|
1660
|
+
} else {
|
|
1661
|
+
const docker = await ctx.out.step(
|
|
1662
|
+
"Starting infra (docker compose up -d)",
|
|
1663
|
+
() => dockerComposeUp(cwd, { quiet: ctx.json })
|
|
1664
|
+
);
|
|
1665
|
+
if (docker.status === "failed") {
|
|
1666
|
+
ctx.out.fail(
|
|
1667
|
+
`${docker.detail}. Is Docker running? Start Docker Desktop (or your docker daemon) and re-run hogsend dev \u2014 or pass --no-infra when infra is managed elsewhere.`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
const envFile = ensureEnvFile(cwd);
|
|
1673
|
+
if (envFile.status === "failed") {
|
|
1674
|
+
ctx.out.fail(
|
|
1675
|
+
`${envFile.detail} \u2014 create a .env (or .env.example) in ${cwd} first.`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
const secret = ensureAuthSecret(cwd);
|
|
1679
|
+
ctx.out.log(color.dim(` env: ${envFile.detail} \xB7 ${secret.detail}`));
|
|
1680
|
+
if (pkg.scripts?.["db:migrate"]) {
|
|
1681
|
+
const migrate = await ctx.out.step(
|
|
1682
|
+
"Running migrations (pnpm db:migrate)",
|
|
1683
|
+
() => runMigrations(cwd, { quiet: ctx.json })
|
|
1684
|
+
);
|
|
1685
|
+
if (migrate.status === "failed") {
|
|
1686
|
+
ctx.out.fail(`${migrate.detail} \u2014 fix, then re-run hogsend dev.`);
|
|
1687
|
+
}
|
|
1688
|
+
} else {
|
|
1689
|
+
ctx.out.log(color.dim(" no db:migrate script \u2014 skipping migrations"));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
async function run4(ctx) {
|
|
1693
|
+
const fire = extractFire(ctx.argv);
|
|
1694
|
+
if (fire && "error" in fire) {
|
|
1695
|
+
ctx.out.fail(fire.error);
|
|
1696
|
+
}
|
|
1697
|
+
if (fire) {
|
|
1698
|
+
await runFire(ctx, fire.event, fire.rest);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const { values: values2 } = parseArgs5({
|
|
1702
|
+
args: ctx.argv,
|
|
1703
|
+
allowPositionals: true,
|
|
1704
|
+
options: {
|
|
1705
|
+
cwd: { type: "string" },
|
|
1706
|
+
"no-worker": { type: "boolean", default: false },
|
|
1707
|
+
"no-infra": { type: "boolean", default: false },
|
|
1708
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
if (values2.help) {
|
|
1712
|
+
ctx.out.log(usage4);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const cwd = resolve(values2.cwd ?? process.cwd());
|
|
1716
|
+
const pkg = assertHogsendApp(cwd, ctx);
|
|
1717
|
+
ctx.out.intro(
|
|
1718
|
+
`${color.bgMagenta(color.black(" hogsend "))} ${color.dim("dev")}`
|
|
1719
|
+
);
|
|
1720
|
+
if (!values2["no-infra"]) {
|
|
1721
|
+
await prepareInfra(ctx, cwd, pkg);
|
|
1722
|
+
}
|
|
1723
|
+
const dotenv = readDotEnv(cwd);
|
|
1724
|
+
const port = parsePort(dotenv.PORT, 3002);
|
|
1725
|
+
const hatchetPort = parsePort(dotenv.HATCHET_DASHBOARD_PORT, 8888);
|
|
1726
|
+
const apiBase = `http://localhost:${port}`;
|
|
1727
|
+
if (await probeTcp({ port })) {
|
|
1728
|
+
ctx.out.fail(
|
|
1729
|
+
`port ${port} is already in use \u2014 is another dev server (or hogsend dev) already running? Stop it, or change PORT in ${join3(cwd, ".env")}.`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
const procs = [];
|
|
1733
|
+
let shuttingDown = false;
|
|
1734
|
+
const shutdown = async (code) => {
|
|
1735
|
+
shuttingDown = true;
|
|
1736
|
+
await shutdownAll(procs);
|
|
1737
|
+
process.exit(code);
|
|
1738
|
+
};
|
|
1739
|
+
process.once("SIGINT", () => {
|
|
1740
|
+
if (shuttingDown) return;
|
|
1741
|
+
ctx.out.log(`
|
|
1742
|
+
${color.dim("Shutting down\u2026")}`);
|
|
1743
|
+
void shutdown(0);
|
|
1744
|
+
});
|
|
1745
|
+
process.once("SIGTERM", () => {
|
|
1746
|
+
if (shuttingDown) return;
|
|
1747
|
+
void shutdown(0);
|
|
1748
|
+
});
|
|
1749
|
+
procs.push(
|
|
1750
|
+
spawnManaged({
|
|
1751
|
+
name: "api",
|
|
1752
|
+
cmd: "pnpm",
|
|
1753
|
+
args: ["run", "dev"],
|
|
1754
|
+
cwd,
|
|
1755
|
+
prefixColor: color.cyan
|
|
1756
|
+
})
|
|
1757
|
+
);
|
|
1758
|
+
if (!values2["no-worker"]) {
|
|
1759
|
+
const useHatchetCli = existsSync3(join3(cwd, "hatchet.yaml")) && hatchetOnPath();
|
|
1760
|
+
const mode = useHatchetCli ? "hatchet worker dev" : "pnpm run worker:dev";
|
|
1761
|
+
ctx.out.log(color.dim(` worker mode: ${mode}`));
|
|
1762
|
+
procs.push(
|
|
1763
|
+
spawnManaged({
|
|
1764
|
+
name: "worker",
|
|
1765
|
+
cmd: useHatchetCli ? "hatchet" : "pnpm",
|
|
1766
|
+
args: useHatchetCli ? ["worker", "dev"] : ["run", "worker:dev"],
|
|
1767
|
+
cwd,
|
|
1768
|
+
prefixColor: color.magenta
|
|
1769
|
+
})
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
for (const proc of procs) {
|
|
1773
|
+
proc.onExit(({ code }) => {
|
|
1774
|
+
if (shuttingDown) return;
|
|
1775
|
+
shuttingDown = true;
|
|
1776
|
+
ctx.out.log(
|
|
1777
|
+
color.red(
|
|
1778
|
+
`
|
|
1779
|
+
[${proc.name}] exited with code ${code ?? "?"} \u2014 shutting down.`
|
|
1780
|
+
)
|
|
1781
|
+
);
|
|
1782
|
+
void shutdownAll(procs).then(() => process.exit(code ?? 1));
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
await ctx.out.step(
|
|
1787
|
+
"Waiting for API health",
|
|
1788
|
+
() => waitForHttp(`${apiBase}/v1/health`, 6e4)
|
|
1789
|
+
);
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
if (shuttingDown) return;
|
|
1792
|
+
shuttingDown = true;
|
|
1793
|
+
await shutdownAll(procs);
|
|
1794
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1795
|
+
ctx.out.fail(
|
|
1796
|
+
`API did not become healthy: ${msg}. Check the [api] log lines above.`
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
const domainLine = await fetchDomainLine(ctx);
|
|
1800
|
+
const lines = [
|
|
1801
|
+
`${color.green("\u25CF")} API ${color.cyan(apiBase)}`,
|
|
1802
|
+
`${color.green("\u25CF")} Studio ${color.cyan(`${apiBase}/studio`)}`,
|
|
1803
|
+
`${color.green("\u25CF")} Hatchet ${color.cyan(`http://localhost:${hatchetPort}`)}`,
|
|
1804
|
+
`${color.green("\u25CF")} Docs ${color.cyan("https://docs.hogsend.com")}`
|
|
1805
|
+
];
|
|
1806
|
+
if (domainLine) lines.push("", domainLine);
|
|
1807
|
+
lines.push(
|
|
1808
|
+
"",
|
|
1809
|
+
`${color.dim("Fire a test event:")} hogsend dev --fire signup --email you@example.com`,
|
|
1810
|
+
color.dim("Press Ctrl+C to stop everything.")
|
|
1811
|
+
);
|
|
1812
|
+
ctx.out.note(lines.join("\n"), "hogsend dev");
|
|
1813
|
+
await new Promise(() => {
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
var devCommand = {
|
|
1817
|
+
name: "dev",
|
|
1818
|
+
summary: "Run the full local stack: infra, API + worker, health, URLs",
|
|
1819
|
+
usage: usage4,
|
|
1820
|
+
run: run4
|
|
1821
|
+
};
|
|
1822
|
+
|
|
822
1823
|
// src/commands/doctor.ts
|
|
823
|
-
import { parseArgs as
|
|
1824
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
824
1825
|
|
|
825
1826
|
// src/lib/skills.ts
|
|
826
1827
|
import {
|
|
827
1828
|
cpSync,
|
|
828
|
-
existsSync,
|
|
1829
|
+
existsSync as existsSync4,
|
|
829
1830
|
mkdirSync,
|
|
830
1831
|
readdirSync,
|
|
831
|
-
readFileSync,
|
|
1832
|
+
readFileSync as readFileSync4,
|
|
832
1833
|
statSync,
|
|
833
|
-
writeFileSync
|
|
1834
|
+
writeFileSync as writeFileSync2
|
|
834
1835
|
} from "fs";
|
|
835
1836
|
import { createRequire } from "module";
|
|
836
|
-
import { join } from "path";
|
|
1837
|
+
import { join as join4 } from "path";
|
|
837
1838
|
import { fileURLToPath } from "url";
|
|
838
1839
|
function bundledSkillsDir() {
|
|
839
1840
|
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
840
1841
|
}
|
|
841
1842
|
function installDir(cwd) {
|
|
842
|
-
return
|
|
1843
|
+
return join4(cwd, ".claude", "skills");
|
|
843
1844
|
}
|
|
844
1845
|
function stampPath(cwd) {
|
|
845
|
-
return
|
|
1846
|
+
return join4(cwd, ".claude", ".hogsend-skills.json");
|
|
846
1847
|
}
|
|
847
1848
|
function cliVersion() {
|
|
848
1849
|
try {
|
|
@@ -855,14 +1856,14 @@ function cliVersion() {
|
|
|
855
1856
|
}
|
|
856
1857
|
function readFileSyncSafe(path) {
|
|
857
1858
|
try {
|
|
858
|
-
return
|
|
1859
|
+
return readFileSync4(path, "utf8");
|
|
859
1860
|
} catch {
|
|
860
1861
|
return "";
|
|
861
1862
|
}
|
|
862
1863
|
}
|
|
863
1864
|
function readFrontmatterField(skillDir, field) {
|
|
864
|
-
const skillFile =
|
|
865
|
-
if (!
|
|
1865
|
+
const skillFile = join4(skillDir, "SKILL.md");
|
|
1866
|
+
if (!existsSync4(skillFile)) return "";
|
|
866
1867
|
const raw = readFileSyncSafe(skillFile);
|
|
867
1868
|
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
868
1869
|
if (!fmMatch) return "";
|
|
@@ -877,22 +1878,22 @@ function readFrontmatterField(skillDir, field) {
|
|
|
877
1878
|
}
|
|
878
1879
|
function listBundledSkills(cwd) {
|
|
879
1880
|
const dir = bundledSkillsDir();
|
|
880
|
-
if (!
|
|
1881
|
+
if (!existsSync4(dir)) return [];
|
|
881
1882
|
const target = installDir(cwd);
|
|
882
1883
|
const entries = readdirSync(dir).filter((name) => {
|
|
883
|
-
const full =
|
|
884
|
-
return statSync(full).isDirectory() &&
|
|
1884
|
+
const full = join4(dir, name);
|
|
1885
|
+
return statSync(full).isDirectory() && existsSync4(join4(full, "SKILL.md"));
|
|
885
1886
|
});
|
|
886
1887
|
return entries.sort().map((name) => ({
|
|
887
1888
|
name,
|
|
888
|
-
description: readFrontmatterField(
|
|
889
|
-
installed:
|
|
1889
|
+
description: readFrontmatterField(join4(dir, name), "description"),
|
|
1890
|
+
installed: existsSync4(join4(target, name))
|
|
890
1891
|
}));
|
|
891
1892
|
}
|
|
892
1893
|
function copySkill(name, cwd, force) {
|
|
893
|
-
const src =
|
|
894
|
-
const dest =
|
|
895
|
-
const exists2 =
|
|
1894
|
+
const src = join4(bundledSkillsDir(), name);
|
|
1895
|
+
const dest = join4(installDir(cwd), name);
|
|
1896
|
+
const exists2 = existsSync4(dest);
|
|
896
1897
|
if (exists2 && !force) {
|
|
897
1898
|
return { name, installed: false, skipped: true, path: dest };
|
|
898
1899
|
}
|
|
@@ -906,13 +1907,13 @@ function writeSkillsStamp(cwd, skills) {
|
|
|
906
1907
|
skills: [...skills].sort(),
|
|
907
1908
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
908
1909
|
};
|
|
909
|
-
mkdirSync(
|
|
910
|
-
|
|
1910
|
+
mkdirSync(join4(cwd, ".claude"), { recursive: true });
|
|
1911
|
+
writeFileSync2(stampPath(cwd), `${JSON.stringify(stamp, null, 2)}
|
|
911
1912
|
`);
|
|
912
1913
|
}
|
|
913
1914
|
function readSkillsStamp(cwd) {
|
|
914
1915
|
try {
|
|
915
|
-
const parsed = JSON.parse(
|
|
1916
|
+
const parsed = JSON.parse(readFileSync4(stampPath(cwd), "utf8"));
|
|
916
1917
|
return parsed && typeof parsed.cliVersion === "string" ? parsed : null;
|
|
917
1918
|
} catch {
|
|
918
1919
|
return null;
|
|
@@ -952,7 +1953,7 @@ function skillsNudge(ctx) {
|
|
|
952
1953
|
"Skills out of date"
|
|
953
1954
|
);
|
|
954
1955
|
}
|
|
955
|
-
var
|
|
1956
|
+
var usage5 = `hogsend doctor [--url <baseUrl>] [--admin-key <key>] [--json]
|
|
956
1957
|
|
|
957
1958
|
Probe a running Hogsend instance via GET /v1/health and report its health:
|
|
958
1959
|
component status (database, redis), two-track schema state (engine + client),
|
|
@@ -994,8 +1995,8 @@ function trackLine(name, track) {
|
|
|
994
1995
|
const required = track.required ?? color.dim("none");
|
|
995
1996
|
return `${color.bold(name.padEnd(7))} applied ${applied} -> required ${required} ${sync}`;
|
|
996
1997
|
}
|
|
997
|
-
async function
|
|
998
|
-
const { values: values2 } =
|
|
1998
|
+
async function run5(ctx) {
|
|
1999
|
+
const { values: values2 } = parseArgs6({
|
|
999
2000
|
args: ctx.argv,
|
|
1000
2001
|
allowPositionals: true,
|
|
1001
2002
|
options: {
|
|
@@ -1005,7 +2006,7 @@ async function run3(ctx) {
|
|
|
1005
2006
|
strict: false
|
|
1006
2007
|
});
|
|
1007
2008
|
if (values2.help) {
|
|
1008
|
-
ctx.out.log(
|
|
2009
|
+
ctx.out.log(usage5);
|
|
1009
2010
|
return;
|
|
1010
2011
|
}
|
|
1011
2012
|
const { baseUrl } = ctx.http.cfg;
|
|
@@ -1066,49 +2067,687 @@ async function run3(ctx) {
|
|
|
1066
2067
|
if (!ok) process.exit(1);
|
|
1067
2068
|
return;
|
|
1068
2069
|
}
|
|
1069
|
-
const
|
|
1070
|
-
ctx.out.intro(
|
|
1071
|
-
const verdictColor = verdict === "ok" ? color.green : verdict === "degraded" ? color.red : color.yellow;
|
|
1072
|
-
const lines = [
|
|
1073
|
-
`${verdictColor("\u25CF")} ${color.bold(verdict)}`,
|
|
1074
|
-
color.dim(
|
|
1075
|
-
`${baseUrl} v${health.version} up ${Math.round(health.uptime)}s`
|
|
1076
|
-
),
|
|
1077
|
-
"",
|
|
1078
|
-
color.bold("Components"),
|
|
1079
|
-
` database ${componentSymbol(health.components.database.status)}${health.components.database.latencyMs !== void 0 ? color.dim(` ${health.components.database.latencyMs}ms`) : ""}`,
|
|
1080
|
-
` redis ${componentSymbol(health.components.redis.status)}${health.components.redis.latencyMs !== void 0 ? color.dim(` ${health.components.redis.latencyMs}ms`) : ""}`,
|
|
1081
|
-
"",
|
|
1082
|
-
color.bold("Schema"),
|
|
1083
|
-
` ${trackLine("engine", health.schema.engine)}`,
|
|
1084
|
-
` ${trackLine("client", health.schema.client)}`
|
|
1085
|
-
];
|
|
1086
|
-
ctx.out.note(lines.join("\n"), "Doctor");
|
|
1087
|
-
skillsNudge(ctx);
|
|
1088
|
-
if (ok) {
|
|
1089
|
-
ctx.out.outro(color.green("doctor: ok"));
|
|
2070
|
+
const badge7 = `${color.bgMagenta(color.black(" hogsend "))} doctor`;
|
|
2071
|
+
ctx.out.intro(badge7);
|
|
2072
|
+
const verdictColor = verdict === "ok" ? color.green : verdict === "degraded" ? color.red : color.yellow;
|
|
2073
|
+
const lines = [
|
|
2074
|
+
`${verdictColor("\u25CF")} ${color.bold(verdict)}`,
|
|
2075
|
+
color.dim(
|
|
2076
|
+
`${baseUrl} v${health.version} up ${Math.round(health.uptime)}s`
|
|
2077
|
+
),
|
|
2078
|
+
"",
|
|
2079
|
+
color.bold("Components"),
|
|
2080
|
+
` database ${componentSymbol(health.components.database.status)}${health.components.database.latencyMs !== void 0 ? color.dim(` ${health.components.database.latencyMs}ms`) : ""}`,
|
|
2081
|
+
` redis ${componentSymbol(health.components.redis.status)}${health.components.redis.latencyMs !== void 0 ? color.dim(` ${health.components.redis.latencyMs}ms`) : ""}`,
|
|
2082
|
+
"",
|
|
2083
|
+
color.bold("Schema"),
|
|
2084
|
+
` ${trackLine("engine", health.schema.engine)}`,
|
|
2085
|
+
` ${trackLine("client", health.schema.client)}`
|
|
2086
|
+
];
|
|
2087
|
+
ctx.out.note(lines.join("\n"), "Doctor");
|
|
2088
|
+
skillsNudge(ctx);
|
|
2089
|
+
if (ok) {
|
|
2090
|
+
ctx.out.outro(color.green("doctor: ok"));
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
ctx.out.outro(verdictColor(`doctor: ${verdict}`));
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
var doctorCommand = {
|
|
2097
|
+
name: "doctor",
|
|
2098
|
+
summary: "Probe a running instance's health (GET /v1/health)",
|
|
2099
|
+
usage: usage5,
|
|
2100
|
+
run: run5
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
// src/commands/domain.ts
|
|
2104
|
+
import { parseArgs as parseArgs7 } from "util";
|
|
2105
|
+
import { confirm } from "@clack/prompts";
|
|
2106
|
+
|
|
2107
|
+
// src/lib/dns.ts
|
|
2108
|
+
import { resolveNs as nodeResolveNs } from "dns/promises";
|
|
2109
|
+
var DNS_HOSTS = {
|
|
2110
|
+
cloudflare: {
|
|
2111
|
+
id: "cloudflare",
|
|
2112
|
+
label: "Cloudflare",
|
|
2113
|
+
nsSuffixes: ["ns.cloudflare.com"],
|
|
2114
|
+
panelUrl: (domain) => `https://dash.cloudflare.com/?to=/:account/${domain}/dns/records`
|
|
2115
|
+
},
|
|
2116
|
+
vercel: {
|
|
2117
|
+
id: "vercel",
|
|
2118
|
+
label: "Vercel",
|
|
2119
|
+
nsSuffixes: ["vercel-dns.com"],
|
|
2120
|
+
panelUrl: (domain) => `https://vercel.com/dashboard/domains/${domain}`
|
|
2121
|
+
},
|
|
2122
|
+
route53: {
|
|
2123
|
+
id: "route53",
|
|
2124
|
+
label: "AWS Route 53",
|
|
2125
|
+
nsSuffixes: ["awsdns-"],
|
|
2126
|
+
panelUrl: () => "https://console.aws.amazon.com/route53/v2/hostedzones"
|
|
2127
|
+
},
|
|
2128
|
+
godaddy: {
|
|
2129
|
+
id: "godaddy",
|
|
2130
|
+
label: "GoDaddy",
|
|
2131
|
+
nsSuffixes: ["domaincontrol.com"],
|
|
2132
|
+
panelUrl: (domain) => `https://dcc.godaddy.com/control/portfolio/${domain}/settings`
|
|
2133
|
+
},
|
|
2134
|
+
namecheap: {
|
|
2135
|
+
id: "namecheap",
|
|
2136
|
+
label: "Namecheap",
|
|
2137
|
+
nsSuffixes: ["registrar-servers.com"],
|
|
2138
|
+
panelUrl: (domain) => `https://ap.www.namecheap.com/domains/domaincontrolpanel/${domain}/advancedns`
|
|
2139
|
+
},
|
|
2140
|
+
porkbun: {
|
|
2141
|
+
id: "porkbun",
|
|
2142
|
+
label: "Porkbun",
|
|
2143
|
+
nsSuffixes: ["porkbun.com"],
|
|
2144
|
+
panelUrl: (domain) => `https://porkbun.com/account/domain/${domain}`
|
|
2145
|
+
},
|
|
2146
|
+
google: {
|
|
2147
|
+
id: "google",
|
|
2148
|
+
label: "Google Domains",
|
|
2149
|
+
nsSuffixes: ["googledomains.com", "google.com"],
|
|
2150
|
+
panelUrl: () => "https://domains.google.com/registrar"
|
|
2151
|
+
},
|
|
2152
|
+
unknown: {
|
|
2153
|
+
id: "unknown",
|
|
2154
|
+
label: "your DNS host",
|
|
2155
|
+
nsSuffixes: [],
|
|
2156
|
+
panelUrl: (domain) => `https://${domain}`
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
async function detectDnsHost(domain, opts = {}) {
|
|
2160
|
+
const resolveNs = opts.resolveNs ?? nodeResolveNs;
|
|
2161
|
+
const labels = domain.toLowerCase().split(".").filter(Boolean);
|
|
2162
|
+
for (let i = 0; i <= labels.length - 2; i++) {
|
|
2163
|
+
const candidate = labels.slice(i).join(".");
|
|
2164
|
+
let nameservers;
|
|
2165
|
+
try {
|
|
2166
|
+
nameservers = await resolveNs(candidate);
|
|
2167
|
+
} catch {
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
if (nameservers.length === 0) continue;
|
|
2171
|
+
const lowered = nameservers.map((ns) => ns.toLowerCase());
|
|
2172
|
+
for (const host of Object.values(DNS_HOSTS)) {
|
|
2173
|
+
if (host.id === "unknown") continue;
|
|
2174
|
+
const matched = host.nsSuffixes.some(
|
|
2175
|
+
(suffix) => lowered.some((ns) => ns.includes(suffix))
|
|
2176
|
+
);
|
|
2177
|
+
if (matched) return host;
|
|
2178
|
+
}
|
|
2179
|
+
return DNS_HOSTS.unknown;
|
|
2180
|
+
}
|
|
2181
|
+
return DNS_HOSTS.unknown;
|
|
2182
|
+
}
|
|
2183
|
+
var RELATIVE_HOST_IDS = /* @__PURE__ */ new Set(["namecheap", "godaddy"]);
|
|
2184
|
+
function relativeName(name, domain) {
|
|
2185
|
+
const lowered = name.toLowerCase();
|
|
2186
|
+
const suffix = `.${domain.toLowerCase()}`;
|
|
2187
|
+
if (lowered === domain.toLowerCase()) return "@";
|
|
2188
|
+
if (lowered.endsWith(suffix))
|
|
2189
|
+
return name.slice(0, name.length - suffix.length);
|
|
2190
|
+
return name;
|
|
2191
|
+
}
|
|
2192
|
+
function renderTable2(rows, header) {
|
|
2193
|
+
const all = [header, ...rows];
|
|
2194
|
+
const widths = header.map(
|
|
2195
|
+
(_, col) => Math.max(...all.map((row) => (row[col] ?? "").length))
|
|
2196
|
+
);
|
|
2197
|
+
const line2 = (row) => row.map((cell, col) => cell.padEnd(widths[col] ?? 0)).join(" ").trimEnd();
|
|
2198
|
+
return [
|
|
2199
|
+
line2(header),
|
|
2200
|
+
line2(widths.map((w) => "-".repeat(w))),
|
|
2201
|
+
...rows.map(line2)
|
|
2202
|
+
].join("\n");
|
|
2203
|
+
}
|
|
2204
|
+
function hostGuidance(host, domain) {
|
|
2205
|
+
switch (host.id) {
|
|
2206
|
+
case "cloudflare":
|
|
2207
|
+
return [
|
|
2208
|
+
"Cloudflare: set Proxy status to DNS only (grey cloud) on every record \u2014",
|
|
2209
|
+
"proxied (orange-cloud) records break email verification."
|
|
2210
|
+
];
|
|
2211
|
+
case "namecheap":
|
|
2212
|
+
case "godaddy":
|
|
2213
|
+
return [
|
|
2214
|
+
`${host.label}: enter the host RELATIVE to ${domain ?? "your domain"} (the table above is already relative).`
|
|
2215
|
+
];
|
|
2216
|
+
case "vercel":
|
|
2217
|
+
return [
|
|
2218
|
+
"Vercel: add the records under the domain's DNS tab (they apply instantly)."
|
|
2219
|
+
];
|
|
2220
|
+
default:
|
|
2221
|
+
return [
|
|
2222
|
+
"Add these records in your DNS host's panel exactly as shown, then run",
|
|
2223
|
+
"`hogsend domain check` to poll verification."
|
|
2224
|
+
];
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
function formatRecordsFor(host, records, opts = {}) {
|
|
2228
|
+
if (records.length === 0) {
|
|
2229
|
+
return "No DNS records reported by the provider yet.";
|
|
2230
|
+
}
|
|
2231
|
+
const relative2 = RELATIVE_HOST_IDS.has(host.id) && opts.domain !== void 0;
|
|
2232
|
+
const rows = records.map((r) => [
|
|
2233
|
+
r.type,
|
|
2234
|
+
relative2 && opts.domain ? relativeName(r.name, opts.domain) : r.name,
|
|
2235
|
+
r.value,
|
|
2236
|
+
r.priority !== void 0 ? String(r.priority) : "",
|
|
2237
|
+
r.status
|
|
2238
|
+
]);
|
|
2239
|
+
const table = renderTable2(rows, [
|
|
2240
|
+
"type",
|
|
2241
|
+
"name",
|
|
2242
|
+
"value",
|
|
2243
|
+
"priority",
|
|
2244
|
+
"status"
|
|
2245
|
+
]);
|
|
2246
|
+
return [table, "", ...hostGuidance(host, opts.domain)].join("\n");
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// src/lib/dns-apply.ts
|
|
2250
|
+
function canAutoApply(host, env) {
|
|
2251
|
+
switch (host) {
|
|
2252
|
+
case "cloudflare":
|
|
2253
|
+
return Boolean(env.CLOUDFLARE_API_TOKEN);
|
|
2254
|
+
case "vercel":
|
|
2255
|
+
return Boolean(env.VERCEL_TOKEN);
|
|
2256
|
+
default:
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
var CF_DUPLICATE_CODES = /* @__PURE__ */ new Set([81057, 81053]);
|
|
2261
|
+
function registrableDomain(domain) {
|
|
2262
|
+
const labels = domain.split(".").filter(Boolean);
|
|
2263
|
+
if (labels.length <= 2) return domain;
|
|
2264
|
+
return labels.slice(-2).join(".");
|
|
2265
|
+
}
|
|
2266
|
+
async function parseJson(res) {
|
|
2267
|
+
try {
|
|
2268
|
+
return await res.json();
|
|
2269
|
+
} catch {
|
|
2270
|
+
return void 0;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
async function applyCloudflare(opts, fetchImpl) {
|
|
2274
|
+
const result = { applied: [], skipped: [], errors: [] };
|
|
2275
|
+
const token = opts.env.CLOUDFLARE_API_TOKEN ?? "";
|
|
2276
|
+
const headers = {
|
|
2277
|
+
Authorization: `Bearer ${token}`,
|
|
2278
|
+
"Content-Type": "application/json"
|
|
2279
|
+
};
|
|
2280
|
+
const zoneName = registrableDomain(opts.domain);
|
|
2281
|
+
let zoneId;
|
|
2282
|
+
try {
|
|
2283
|
+
const res = await fetchImpl(
|
|
2284
|
+
`https://api.cloudflare.com/client/v4/zones?name=${encodeURIComponent(zoneName)}`,
|
|
2285
|
+
{ headers }
|
|
2286
|
+
);
|
|
2287
|
+
const body = await parseJson(res);
|
|
2288
|
+
zoneId = body?.result?.[0]?.id;
|
|
2289
|
+
} catch (cause) {
|
|
2290
|
+
result.errors.push(
|
|
2291
|
+
`Cloudflare zone lookup failed: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
2292
|
+
);
|
|
2293
|
+
return result;
|
|
2294
|
+
}
|
|
2295
|
+
if (!zoneId) {
|
|
2296
|
+
result.errors.push(
|
|
2297
|
+
`could not resolve a Cloudflare zone for ${zoneName} \u2014 is the domain on this account?`
|
|
2298
|
+
);
|
|
2299
|
+
return result;
|
|
2300
|
+
}
|
|
2301
|
+
for (const record of opts.records) {
|
|
2302
|
+
try {
|
|
2303
|
+
const res = await fetchImpl(
|
|
2304
|
+
`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`,
|
|
2305
|
+
{
|
|
2306
|
+
method: "POST",
|
|
2307
|
+
headers,
|
|
2308
|
+
body: JSON.stringify({
|
|
2309
|
+
type: record.type,
|
|
2310
|
+
name: record.name,
|
|
2311
|
+
content: record.value,
|
|
2312
|
+
ttl: record.ttl ?? 1,
|
|
2313
|
+
// 1 = automatic
|
|
2314
|
+
...record.priority !== void 0 ? { priority: record.priority } : {},
|
|
2315
|
+
// NEVER proxy mail-verification records — orange-cloud breaks them.
|
|
2316
|
+
proxied: false
|
|
2317
|
+
})
|
|
2318
|
+
}
|
|
2319
|
+
);
|
|
2320
|
+
if (res.ok) {
|
|
2321
|
+
result.applied.push(record);
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
const body = await parseJson(res);
|
|
2325
|
+
const codes = (body?.errors ?? []).map((e) => e.code ?? 0);
|
|
2326
|
+
if (codes.some((code) => CF_DUPLICATE_CODES.has(code))) {
|
|
2327
|
+
result.skipped.push(record);
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
const message = body?.errors?.[0]?.message ?? `Cloudflare API status ${res.status}`;
|
|
2331
|
+
result.errors.push(`${record.type} ${record.name}: ${message}`);
|
|
2332
|
+
} catch (cause) {
|
|
2333
|
+
result.errors.push(
|
|
2334
|
+
`${record.type} ${record.name}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return result;
|
|
2339
|
+
}
|
|
2340
|
+
async function applyVercel(opts, fetchImpl) {
|
|
2341
|
+
const result = { applied: [], skipped: [], errors: [] };
|
|
2342
|
+
const token = opts.env.VERCEL_TOKEN ?? "";
|
|
2343
|
+
const teamId = opts.env.VERCEL_TEAM_ID;
|
|
2344
|
+
const base = `https://api.vercel.com/v2/domains/${encodeURIComponent(opts.domain)}/records`;
|
|
2345
|
+
const url = teamId ? `${base}?teamId=${encodeURIComponent(teamId)}` : base;
|
|
2346
|
+
for (const record of opts.records) {
|
|
2347
|
+
try {
|
|
2348
|
+
const res = await fetchImpl(url, {
|
|
2349
|
+
method: "POST",
|
|
2350
|
+
headers: {
|
|
2351
|
+
Authorization: `Bearer ${token}`,
|
|
2352
|
+
"Content-Type": "application/json"
|
|
2353
|
+
},
|
|
2354
|
+
body: JSON.stringify({
|
|
2355
|
+
type: record.type,
|
|
2356
|
+
name: record.name,
|
|
2357
|
+
value: record.value,
|
|
2358
|
+
...record.ttl !== void 0 ? { ttl: record.ttl } : {},
|
|
2359
|
+
...record.priority !== void 0 ? { mxPriority: record.priority } : {}
|
|
2360
|
+
})
|
|
2361
|
+
});
|
|
2362
|
+
if (res.ok) {
|
|
2363
|
+
result.applied.push(record);
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
const body = await parseJson(res);
|
|
2367
|
+
const code = body?.error?.code ?? "";
|
|
2368
|
+
const message = body?.error?.message ?? "";
|
|
2369
|
+
if (/duplicate/i.test(code) || /already exists/i.test(message)) {
|
|
2370
|
+
result.skipped.push(record);
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
result.errors.push(
|
|
2374
|
+
`${record.type} ${record.name}: ${message || `Vercel API status ${res.status}`}`
|
|
2375
|
+
);
|
|
2376
|
+
} catch (cause) {
|
|
2377
|
+
result.errors.push(
|
|
2378
|
+
`${record.type} ${record.name}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return result;
|
|
2383
|
+
}
|
|
2384
|
+
async function applyRecords(opts) {
|
|
2385
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
2386
|
+
if (!canAutoApply(opts.host, opts.env)) {
|
|
2387
|
+
return {
|
|
2388
|
+
applied: [],
|
|
2389
|
+
skipped: [...opts.records],
|
|
2390
|
+
errors: [
|
|
2391
|
+
`auto-apply is not available for ${opts.host} \u2014 add the records manually in your DNS panel`
|
|
2392
|
+
]
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
if (opts.host === "cloudflare") return applyCloudflare(opts, fetchImpl);
|
|
2396
|
+
return applyVercel(opts, fetchImpl);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
// src/lib/prompt.ts
|
|
2400
|
+
import { cancel as cancel2, isCancel } from "@clack/prompts";
|
|
2401
|
+
function bail(value) {
|
|
2402
|
+
if (isCancel(value)) {
|
|
2403
|
+
cancel2("Cancelled.");
|
|
2404
|
+
process.exit(0);
|
|
2405
|
+
}
|
|
2406
|
+
return value;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// src/commands/domain.ts
|
|
2410
|
+
var usage6 = `hogsend domain <subcommand> [options]
|
|
2411
|
+
|
|
2412
|
+
Manage the sending domain through the RUNNING instance's admin routes
|
|
2413
|
+
(/v1/admin/domain) \u2014 provider API keys never touch the CLI. Requires an admin
|
|
2414
|
+
key (--admin-key / HOGSEND_ADMIN_KEY).
|
|
2415
|
+
|
|
2416
|
+
Subcommands:
|
|
2417
|
+
add <domain> Register the domain with the email provider, then print
|
|
2418
|
+
the DNS records formatted for YOUR DNS host (detected
|
|
2419
|
+
via NS lookup) with a panel deep link. When a
|
|
2420
|
+
CLOUDFLARE_API_TOKEN / VERCEL_TOKEN is set, offers to
|
|
2421
|
+
apply the records automatically.
|
|
2422
|
+
check [<domain>] Trigger a provider verification pass, then poll status
|
|
2423
|
+
every 15s until verified (exit 0) or timeout (exit 1).
|
|
2424
|
+
status Show domain, provider, verification state, DNS records,
|
|
2425
|
+
and the test-mode banner.
|
|
2426
|
+
|
|
2427
|
+
add options:
|
|
2428
|
+
--apply Apply records via the DNS host API without prompting.
|
|
2429
|
+
--no-apply Never apply records (skip the prompt).
|
|
2430
|
+
|
|
2431
|
+
check options:
|
|
2432
|
+
--timeout <s> Give up after this many seconds (default 300).
|
|
2433
|
+
--once Poll exactly once; exit per the current state.
|
|
2434
|
+
|
|
2435
|
+
status options:
|
|
2436
|
+
--refresh Bypass the server-side cache (forces a provider call).
|
|
2437
|
+
|
|
2438
|
+
Global options (handled by the router): --url, --admin-key, --json, -h/--help.
|
|
2439
|
+
|
|
2440
|
+
Examples:
|
|
2441
|
+
hogsend domain add mysite.com
|
|
2442
|
+
hogsend domain add mysite.com --apply
|
|
2443
|
+
hogsend domain check --timeout 600
|
|
2444
|
+
hogsend domain status --json`;
|
|
2445
|
+
var badge3 = `${color.bgMagenta(color.black(" hogsend "))} domain`;
|
|
2446
|
+
var DOMAIN_RE = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
2447
|
+
var POLL_INTERVAL_MS = 15e3;
|
|
2448
|
+
var sleep2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2449
|
+
function getStatus(ctx, opts = {}) {
|
|
2450
|
+
const query = opts.refresh ? { refresh: "true" } : void 0;
|
|
2451
|
+
return ctx.http.get("/v1/admin/domain", query);
|
|
2452
|
+
}
|
|
2453
|
+
async function providerLabel(ctx) {
|
|
2454
|
+
try {
|
|
2455
|
+
const status = await getStatus(ctx);
|
|
2456
|
+
return status.providerId;
|
|
2457
|
+
} catch {
|
|
2458
|
+
return "the active email provider";
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
async function failUnsupported(ctx, err) {
|
|
2462
|
+
if (isHttpError(err) && err.status === 501) {
|
|
2463
|
+
const provider = await providerLabel(ctx);
|
|
2464
|
+
ctx.out.fail(
|
|
2465
|
+
`provider ${provider} does not support domain management \u2014 verify the domain in your provider's dashboard instead`
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
throw err;
|
|
2469
|
+
}
|
|
2470
|
+
function recordTicks(records) {
|
|
2471
|
+
return records.map((r) => {
|
|
2472
|
+
const tick = r.status === "verified" ? color.green("\u2713") : r.status === "failed" ? color.red("\u2717") : color.yellow("\u2026");
|
|
2473
|
+
return ` ${tick} ${r.type.padEnd(5)} ${r.name} ${color.dim(r.status)}`;
|
|
2474
|
+
}).join("\n");
|
|
2475
|
+
}
|
|
2476
|
+
function renderStatus(ctx, status) {
|
|
2477
|
+
ctx.out.kv({
|
|
2478
|
+
domain: status.domain ?? "(not configured)",
|
|
2479
|
+
provider: status.providerId,
|
|
2480
|
+
supported: status.supported,
|
|
2481
|
+
state: status.status?.state ?? "n/a",
|
|
2482
|
+
checkedAt: status.status?.checkedAt ?? ""
|
|
2483
|
+
});
|
|
2484
|
+
const records = status.status?.records ?? [];
|
|
2485
|
+
if (records.length > 0) {
|
|
2486
|
+
ctx.out.log("");
|
|
2487
|
+
ctx.out.table(
|
|
2488
|
+
records.map((r) => ({
|
|
2489
|
+
type: r.type,
|
|
2490
|
+
name: r.name,
|
|
2491
|
+
value: r.value,
|
|
2492
|
+
priority: r.priority ?? "",
|
|
2493
|
+
purpose: r.purpose,
|
|
2494
|
+
status: r.status
|
|
2495
|
+
}))
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
if (status.testMode.active) {
|
|
2499
|
+
ctx.out.log("");
|
|
2500
|
+
ctx.out.log(
|
|
2501
|
+
`${color.bgYellow(color.black(" TEST MODE "))} ${color.yellow(
|
|
2502
|
+
`all sends redirect to ${status.testMode.redirectTo ?? "(no redirect address!)"} \u2014 reason: ${status.testMode.reason ?? "unknown"}`
|
|
2503
|
+
)}`
|
|
2504
|
+
);
|
|
2505
|
+
if (!status.testMode.redirectTo) {
|
|
2506
|
+
ctx.out.log(
|
|
2507
|
+
color.dim(
|
|
2508
|
+
" set HOGSEND_TEST_EMAIL (or STUDIO_ADMIN_EMAIL) \u2014 sends are BLOCKED until one is configured"
|
|
2509
|
+
)
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
} else if (status.status?.state === "verified") {
|
|
2513
|
+
ctx.out.log("");
|
|
2514
|
+
ctx.out.log(`${color.green("\u2713")} sends live`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
async function runAdd(ctx, argv) {
|
|
2518
|
+
const { values: values2, positionals } = parseArgs7({
|
|
2519
|
+
args: argv,
|
|
2520
|
+
allowPositionals: true,
|
|
2521
|
+
options: {
|
|
2522
|
+
apply: { type: "boolean", default: false },
|
|
2523
|
+
"no-apply": { type: "boolean", default: false },
|
|
2524
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
if (values2.help) {
|
|
2528
|
+
ctx.out.log(usage6);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const domain = positionals[0];
|
|
2532
|
+
if (!domain) {
|
|
2533
|
+
ctx.out.fail("missing <domain> \u2014 usage: hogsend domain add <domain>");
|
|
2534
|
+
}
|
|
2535
|
+
if (!DOMAIN_RE.test(domain)) {
|
|
2536
|
+
ctx.out.fail(`invalid domain "${domain}" (expected e.g. mysite.com)`);
|
|
2537
|
+
}
|
|
2538
|
+
ctx.out.intro(badge3);
|
|
2539
|
+
let status;
|
|
2540
|
+
try {
|
|
2541
|
+
status = await ctx.out.step(
|
|
2542
|
+
`Registering ${domain} with the provider`,
|
|
2543
|
+
() => ctx.http.post("/v1/admin/domain", { domain })
|
|
2544
|
+
);
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
return failUnsupported(ctx, err);
|
|
2547
|
+
}
|
|
2548
|
+
const records = status.status?.records ?? [];
|
|
2549
|
+
const host = await detectDnsHost(domain);
|
|
2550
|
+
let applyResult = null;
|
|
2551
|
+
const autoAvailable = canAutoApply(host.id, process.env);
|
|
2552
|
+
if (autoAvailable && records.length > 0) {
|
|
2553
|
+
const doApply = values2.apply ? true : values2["no-apply"] ? false : ctx.out.interactive ? bail(
|
|
2554
|
+
await confirm({
|
|
2555
|
+
message: `Apply these records via the ${host.label} API?`,
|
|
2556
|
+
initialValue: true
|
|
2557
|
+
})
|
|
2558
|
+
) : false;
|
|
2559
|
+
if (doApply) {
|
|
2560
|
+
applyResult = await ctx.out.step(
|
|
2561
|
+
`Applying ${records.length} record(s) via ${host.label}`,
|
|
2562
|
+
() => applyRecords({ host: host.id, domain, records, env: process.env })
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (ctx.json) {
|
|
2567
|
+
ctx.out.json({
|
|
2568
|
+
status,
|
|
2569
|
+
dnsHost: host.id,
|
|
2570
|
+
panelUrl: host.panelUrl(domain),
|
|
2571
|
+
autoApplyAvailable: autoAvailable,
|
|
2572
|
+
applied: applyResult
|
|
2573
|
+
});
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
ctx.out.log("");
|
|
2577
|
+
ctx.out.log(formatRecordsFor(host, records, { domain }));
|
|
2578
|
+
ctx.out.log("");
|
|
2579
|
+
ctx.out.log(
|
|
2580
|
+
`${color.dim("DNS panel:")} ${color.cyan(host.panelUrl(domain))}`
|
|
2581
|
+
);
|
|
2582
|
+
if (applyResult) {
|
|
2583
|
+
ctx.out.log("");
|
|
2584
|
+
ctx.out.log(
|
|
2585
|
+
`${color.green("applied")} ${applyResult.applied.length} ${color.yellow("skipped")} ${applyResult.skipped.length} ${color.red("errors")} ${applyResult.errors.length}`
|
|
2586
|
+
);
|
|
2587
|
+
for (const error of applyResult.errors) {
|
|
2588
|
+
ctx.out.log(` ${color.red("\u2717")} ${error}`);
|
|
2589
|
+
}
|
|
2590
|
+
} else if (autoAvailable && records.length > 0) {
|
|
2591
|
+
ctx.out.log("");
|
|
2592
|
+
ctx.out.log(
|
|
2593
|
+
color.dim(
|
|
2594
|
+
`Auto-apply available \u2014 rerun with --apply to write them via ${host.label}.`
|
|
2595
|
+
)
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
ctx.out.outro(
|
|
2599
|
+
`Records added? Run ${color.cyan("hogsend domain check")} to poll verification.`
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
async function runCheck(ctx, argv) {
|
|
2603
|
+
const { values: values2, positionals } = parseArgs7({
|
|
2604
|
+
args: argv,
|
|
2605
|
+
allowPositionals: true,
|
|
2606
|
+
options: {
|
|
2607
|
+
timeout: { type: "string", default: "300" },
|
|
2608
|
+
once: { type: "boolean", default: false },
|
|
2609
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
if (values2.help) {
|
|
2613
|
+
ctx.out.log(usage6);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
const timeoutSecs = Number(values2.timeout);
|
|
2617
|
+
if (!Number.isFinite(timeoutSecs) || timeoutSecs <= 0) {
|
|
2618
|
+
ctx.out.fail(`invalid --timeout "${values2.timeout}" (expected seconds)`);
|
|
2619
|
+
}
|
|
2620
|
+
ctx.out.intro(badge3);
|
|
2621
|
+
const requested = positionals[0];
|
|
2622
|
+
if (requested) {
|
|
2623
|
+
const current = await getStatus(ctx);
|
|
2624
|
+
if (current.domain && current.domain !== requested.toLowerCase()) {
|
|
2625
|
+
ctx.out.log(
|
|
2626
|
+
color.yellow(
|
|
2627
|
+
`note: the instance's configured sending domain is ${current.domain}; checking that (not ${requested}). Set EMAIL_DOMAIN to change it.`
|
|
2628
|
+
)
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
try {
|
|
2633
|
+
await ctx.out.step(
|
|
2634
|
+
"Triggering provider verification",
|
|
2635
|
+
() => ctx.http.post("/v1/admin/domain/verify", {})
|
|
2636
|
+
);
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
if (isHttpError(err) && err.status === 400) {
|
|
2639
|
+
ctx.out.fail(
|
|
2640
|
+
"no sending domain configured \u2014 set EMAIL_DOMAIN (or EMAIL_FROM), or run `hogsend domain add <domain>` first"
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
return failUnsupported(ctx, err);
|
|
2644
|
+
}
|
|
2645
|
+
const deadline = Date.now() + timeoutSecs * 1e3;
|
|
2646
|
+
for (; ; ) {
|
|
2647
|
+
const status = await getStatus(ctx, { refresh: true });
|
|
2648
|
+
const records = status.status?.records ?? [];
|
|
2649
|
+
if (records.length > 0) {
|
|
2650
|
+
ctx.out.log(recordTicks(records));
|
|
2651
|
+
}
|
|
2652
|
+
if (status.status?.state === "verified") {
|
|
2653
|
+
if (ctx.json) {
|
|
2654
|
+
ctx.out.json(status);
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
renderStatus(ctx, status);
|
|
2658
|
+
ctx.out.outro(`${color.green("Verified.")} ${status.domain} is live.`);
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (values2.once) {
|
|
2662
|
+
if (ctx.json) {
|
|
2663
|
+
ctx.out.json(status);
|
|
2664
|
+
process.exitCode = 1;
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
ctx.out.fail(
|
|
2668
|
+
`domain is ${status.status?.state ?? "not configured"} (not verified)`
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
if (Date.now() + POLL_INTERVAL_MS > deadline) {
|
|
2672
|
+
ctx.out.fail(
|
|
2673
|
+
`timed out after ${timeoutSecs}s \u2014 DNS can take a while to propagate; rerun \`hogsend domain check\` later`
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
ctx.out.log(
|
|
2677
|
+
color.dim(
|
|
2678
|
+
`state: ${status.status?.state ?? "unknown"} \u2014 polling again in 15s ...`
|
|
2679
|
+
)
|
|
2680
|
+
);
|
|
2681
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
async function runStatus2(ctx, argv) {
|
|
2685
|
+
const { values: values2 } = parseArgs7({
|
|
2686
|
+
args: argv,
|
|
2687
|
+
allowPositionals: false,
|
|
2688
|
+
options: {
|
|
2689
|
+
refresh: { type: "boolean", default: false },
|
|
2690
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2691
|
+
}
|
|
2692
|
+
});
|
|
2693
|
+
if (values2.help) {
|
|
2694
|
+
ctx.out.log(usage6);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const status = await getStatus(ctx, { refresh: values2.refresh });
|
|
2698
|
+
if (ctx.json) {
|
|
2699
|
+
ctx.out.json(status);
|
|
1090
2700
|
return;
|
|
1091
2701
|
}
|
|
1092
|
-
ctx.out.
|
|
1093
|
-
|
|
2702
|
+
ctx.out.intro(badge3);
|
|
2703
|
+
renderStatus(ctx, status);
|
|
2704
|
+
if (!status.supported) {
|
|
2705
|
+
ctx.out.log("");
|
|
2706
|
+
ctx.out.log(
|
|
2707
|
+
color.dim(
|
|
2708
|
+
`provider ${status.providerId} does not support domain management \u2014 verify the domain in your provider's dashboard`
|
|
2709
|
+
)
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
ctx.out.outro("Done.");
|
|
1094
2713
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
2714
|
+
async function run6(ctx) {
|
|
2715
|
+
const sub = ctx.argv[0];
|
|
2716
|
+
const rest = ctx.argv.slice(1);
|
|
2717
|
+
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
2718
|
+
ctx.out.log(usage6);
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
switch (sub) {
|
|
2722
|
+
case "add":
|
|
2723
|
+
return runAdd(ctx, rest);
|
|
2724
|
+
case "check":
|
|
2725
|
+
return runCheck(ctx, rest);
|
|
2726
|
+
case "status":
|
|
2727
|
+
return runStatus2(ctx, rest);
|
|
2728
|
+
default:
|
|
2729
|
+
ctx.out.fail(
|
|
2730
|
+
`unknown subcommand "${sub}" \u2014 expected add | check | status`
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
var domainCommand = {
|
|
2735
|
+
name: "domain",
|
|
2736
|
+
summary: "Set up + verify the sending domain (DNS records, auto-apply)",
|
|
2737
|
+
usage: usage6,
|
|
2738
|
+
run: run6
|
|
1100
2739
|
};
|
|
1101
2740
|
|
|
1102
2741
|
// src/commands/eject.ts
|
|
1103
|
-
import { existsSync as
|
|
2742
|
+
import { existsSync as existsSync6, realpathSync } from "fs";
|
|
1104
2743
|
import { createRequire as createRequire2 } from "module";
|
|
1105
|
-
import { dirname, join as
|
|
1106
|
-
import { parseArgs as
|
|
2744
|
+
import { dirname, join as join6, sep as sep2 } from "path";
|
|
2745
|
+
import { parseArgs as parseArgs8 } from "util";
|
|
1107
2746
|
|
|
1108
2747
|
// src/eject.ts
|
|
1109
|
-
import { existsSync as
|
|
2748
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1110
2749
|
import { cp, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1111
|
-
import { basename, join as
|
|
2750
|
+
import { basename, join as join5, relative, sep } from "path";
|
|
1112
2751
|
var EjectError = class extends Error {
|
|
1113
2752
|
constructor(message) {
|
|
1114
2753
|
super(message);
|
|
@@ -1133,8 +2772,8 @@ async function writePackageJson(file, value) {
|
|
|
1133
2772
|
async function eject(opts) {
|
|
1134
2773
|
const { pkg, consumerRoot, sourceDir, force = false } = opts;
|
|
1135
2774
|
const vendorName = basename(pkg);
|
|
1136
|
-
const vendorPath =
|
|
1137
|
-
const consumerPkgPath =
|
|
2775
|
+
const vendorPath = join5(consumerRoot, "vendor", vendorName);
|
|
2776
|
+
const consumerPkgPath = join5(consumerRoot, "package.json");
|
|
1138
2777
|
const consumerPkg = await readPackageJson(consumerPkgPath);
|
|
1139
2778
|
let depMap;
|
|
1140
2779
|
let depSpecBefore;
|
|
@@ -1150,7 +2789,7 @@ async function eject(opts) {
|
|
|
1150
2789
|
`${pkg} is not a dependency of the consumer package.json`
|
|
1151
2790
|
);
|
|
1152
2791
|
}
|
|
1153
|
-
if (
|
|
2792
|
+
if (existsSync5(vendorPath)) {
|
|
1154
2793
|
if (!force) {
|
|
1155
2794
|
throw new EjectError(
|
|
1156
2795
|
`vendor/${vendorName} already exists; pass --force to overwrite`
|
|
@@ -1178,7 +2817,7 @@ async function eject(opts) {
|
|
|
1178
2817
|
}
|
|
1179
2818
|
});
|
|
1180
2819
|
copiedFiles = await countFiles(vendorPath);
|
|
1181
|
-
const vendoredPkgPath =
|
|
2820
|
+
const vendoredPkgPath = join5(vendorPath, "package.json");
|
|
1182
2821
|
const vendoredPkg = await readPackageJson(vendoredPkgPath);
|
|
1183
2822
|
if (vendoredPkg.private === true) {
|
|
1184
2823
|
delete vendoredPkg.private;
|
|
@@ -1201,7 +2840,7 @@ async function countFiles(dir) {
|
|
|
1201
2840
|
let count = 0;
|
|
1202
2841
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
1203
2842
|
for (const entry of entries) {
|
|
1204
|
-
const full =
|
|
2843
|
+
const full = join5(dir, entry.name);
|
|
1205
2844
|
if (entry.isDirectory()) {
|
|
1206
2845
|
count += await countFiles(full);
|
|
1207
2846
|
} else if (entry.isFile()) {
|
|
@@ -1217,7 +2856,7 @@ async function countFiles(dir) {
|
|
|
1217
2856
|
}
|
|
1218
2857
|
|
|
1219
2858
|
// src/commands/eject.ts
|
|
1220
|
-
var
|
|
2859
|
+
var usage7 = `hogsend eject <package> [--force] [--cwd <dir>]
|
|
1221
2860
|
|
|
1222
2861
|
Copy a @hogsend/* package's source into vendor/<name> and rewrite the consumer
|
|
1223
2862
|
dependency to file:./vendor/<name>. Every other dependency keeps upgrading.
|
|
@@ -1229,8 +2868,8 @@ Options:
|
|
|
1229
2868
|
|
|
1230
2869
|
After ejecting, run: pnpm install`;
|
|
1231
2870
|
function resolveSourceDir(pkg, consumerRoot) {
|
|
1232
|
-
const direct =
|
|
1233
|
-
if (
|
|
2871
|
+
const direct = join6(consumerRoot, "node_modules", pkg, "package.json");
|
|
2872
|
+
if (existsSync6(direct)) {
|
|
1234
2873
|
return dirname(realpathSync(direct));
|
|
1235
2874
|
}
|
|
1236
2875
|
const require2 = createRequire2(`${consumerRoot}${sep2}`);
|
|
@@ -1238,15 +2877,15 @@ function resolveSourceDir(pkg, consumerRoot) {
|
|
|
1238
2877
|
const entry = require2.resolve(pkg);
|
|
1239
2878
|
let dir = dirname(entry);
|
|
1240
2879
|
while (dir !== dirname(dir)) {
|
|
1241
|
-
if (
|
|
2880
|
+
if (existsSync6(join6(dir, "package.json"))) return dir;
|
|
1242
2881
|
dir = dirname(dir);
|
|
1243
2882
|
}
|
|
1244
2883
|
} catch {
|
|
1245
2884
|
}
|
|
1246
2885
|
return null;
|
|
1247
2886
|
}
|
|
1248
|
-
async function
|
|
1249
|
-
const { values: values2, positionals } =
|
|
2887
|
+
async function run7(ctx) {
|
|
2888
|
+
const { values: values2, positionals } = parseArgs8({
|
|
1250
2889
|
args: ctx.argv,
|
|
1251
2890
|
allowPositionals: true,
|
|
1252
2891
|
options: {
|
|
@@ -1256,7 +2895,7 @@ async function run4(ctx) {
|
|
|
1256
2895
|
}
|
|
1257
2896
|
});
|
|
1258
2897
|
if (values2.help) {
|
|
1259
|
-
ctx.out.log(
|
|
2898
|
+
ctx.out.log(usage7);
|
|
1260
2899
|
return;
|
|
1261
2900
|
}
|
|
1262
2901
|
const pkg = positionals[0];
|
|
@@ -1300,13 +2939,13 @@ async function run4(ctx) {
|
|
|
1300
2939
|
var ejectCommand = {
|
|
1301
2940
|
name: "eject",
|
|
1302
2941
|
summary: "Vendor a @hogsend/* package into vendor/<name>",
|
|
1303
|
-
usage:
|
|
1304
|
-
run:
|
|
2942
|
+
usage: usage7,
|
|
2943
|
+
run: run7
|
|
1305
2944
|
};
|
|
1306
2945
|
|
|
1307
2946
|
// src/commands/emails.ts
|
|
1308
|
-
import { parseArgs as
|
|
1309
|
-
var
|
|
2947
|
+
import { parseArgs as parseArgs9 } from "util";
|
|
2948
|
+
var usage8 = `hogsend emails <subcommand> [options]
|
|
1310
2949
|
|
|
1311
2950
|
Send a transactional email through the data plane (POST /v1/emails). The send
|
|
1312
2951
|
runs through the full preferences + tracking pipeline (link-click + open).
|
|
@@ -1334,330 +2973,106 @@ Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
|
1334
2973
|
Examples:
|
|
1335
2974
|
hogsend emails send welcome --to a@b.com --prop name=Ada
|
|
1336
2975
|
hogsend emails send welcome --user-id user_123 --props '{"name":"Ada"}' --json`;
|
|
1337
|
-
var
|
|
1338
|
-
function
|
|
2976
|
+
var badge4 = `${color.bgMagenta(color.black(" hogsend "))} emails`;
|
|
2977
|
+
function parseProps4(ctx, propsJson, propPairs) {
|
|
1339
2978
|
const out = {};
|
|
1340
2979
|
let any = false;
|
|
1341
2980
|
if (propsJson !== void 0) {
|
|
1342
2981
|
let parsed;
|
|
1343
2982
|
try {
|
|
1344
2983
|
parsed = JSON.parse(propsJson);
|
|
1345
|
-
} catch {
|
|
1346
|
-
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
1347
|
-
}
|
|
1348
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1349
|
-
ctx.out.fail("--props must be a JSON object");
|
|
1350
|
-
}
|
|
1351
|
-
Object.assign(out, parsed);
|
|
1352
|
-
any = true;
|
|
1353
|
-
}
|
|
1354
|
-
for (const pair of propPairs ?? []) {
|
|
1355
|
-
const eq2 = pair.indexOf("=");
|
|
1356
|
-
if (eq2 === -1) {
|
|
1357
|
-
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
1358
|
-
}
|
|
1359
|
-
const key = pair.slice(0, eq2).trim();
|
|
1360
|
-
if (key === "") {
|
|
1361
|
-
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
1362
|
-
}
|
|
1363
|
-
out[key] = coerceValue3(pair.slice(eq2 + 1));
|
|
1364
|
-
any = true;
|
|
1365
|
-
}
|
|
1366
|
-
return any ? out : void 0;
|
|
1367
|
-
}
|
|
1368
|
-
function coerceValue3(raw) {
|
|
1369
|
-
try {
|
|
1370
|
-
return JSON.parse(raw);
|
|
1371
|
-
} catch {
|
|
1372
|
-
return raw;
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
function statusColor2(status) {
|
|
1376
|
-
switch (status) {
|
|
1377
|
-
case "queued":
|
|
1378
|
-
case "sent":
|
|
1379
|
-
return color.green(status);
|
|
1380
|
-
case "skipped":
|
|
1381
|
-
return color.dim(status);
|
|
1382
|
-
default:
|
|
1383
|
-
return color.yellow(status);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
async function runSend2(ctx, argv) {
|
|
1387
|
-
const { values: values2, positionals } = parseArgs5({
|
|
1388
|
-
args: argv,
|
|
1389
|
-
allowPositionals: true,
|
|
1390
|
-
options: {
|
|
1391
|
-
to: { type: "string" },
|
|
1392
|
-
"user-id": { type: "string" },
|
|
1393
|
-
prop: { type: "string", multiple: true },
|
|
1394
|
-
props: { type: "string" },
|
|
1395
|
-
from: { type: "string" },
|
|
1396
|
-
subject: { type: "string" },
|
|
1397
|
-
"reply-to": { type: "string" },
|
|
1398
|
-
category: { type: "string" },
|
|
1399
|
-
"skip-preference-check": { type: "boolean", default: false },
|
|
1400
|
-
"idempotency-key": { type: "string" },
|
|
1401
|
-
help: { type: "boolean", short: "h", default: false }
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1404
|
-
if (values2.help) {
|
|
1405
|
-
ctx.out.log(usage5);
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
const template = positionals[0];
|
|
1409
|
-
if (!template) {
|
|
1410
|
-
ctx.out.fail(
|
|
1411
|
-
"emails send requires a template, e.g. hogsend emails send welcome --to a@b.com"
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
|
-
const to = values2.to;
|
|
1415
|
-
const userId = values2["user-id"];
|
|
1416
|
-
if (!to && !userId) {
|
|
1417
|
-
ctx.out.fail("emails send requires at least one of --to or --user-id");
|
|
1418
|
-
}
|
|
1419
|
-
const props = parseProps3(ctx, values2.props, values2.prop);
|
|
1420
|
-
const body = { template };
|
|
1421
|
-
if (to) body.to = to;
|
|
1422
|
-
if (userId) body.userId = userId;
|
|
1423
|
-
if (props) body.props = props;
|
|
1424
|
-
if (values2.from) body.from = values2.from;
|
|
1425
|
-
if (values2.subject) body.subject = values2.subject;
|
|
1426
|
-
if (values2["reply-to"]) body.replyTo = values2["reply-to"];
|
|
1427
|
-
if (values2.category) body.category = values2.category;
|
|
1428
|
-
if (values2["skip-preference-check"]) body.skipPreferenceCheck = true;
|
|
1429
|
-
if (values2["idempotency-key"]) {
|
|
1430
|
-
body.idempotencyKey = values2["idempotency-key"];
|
|
1431
|
-
}
|
|
1432
|
-
let res;
|
|
1433
|
-
try {
|
|
1434
|
-
res = await ctx.out.step(
|
|
1435
|
-
`Sending ${template}`,
|
|
1436
|
-
() => ctx.dataHttp.post("/v1/emails", body)
|
|
1437
|
-
);
|
|
1438
|
-
} catch (error) {
|
|
1439
|
-
if (isHttpError(error)) {
|
|
1440
|
-
ctx.out.fail(error.message);
|
|
1441
|
-
}
|
|
1442
|
-
throw error;
|
|
1443
|
-
}
|
|
1444
|
-
if (ctx.json) {
|
|
1445
|
-
ctx.out.json(res);
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
ctx.out.intro(`${badge3} send`);
|
|
1449
|
-
ctx.out.kv(
|
|
1450
|
-
{
|
|
1451
|
-
emailSendId: res.emailSendId,
|
|
1452
|
-
template,
|
|
1453
|
-
recipient: to ?? userId ?? "",
|
|
1454
|
-
status: statusColor2(res.status),
|
|
1455
|
-
reason: res.reason ?? ""
|
|
1456
|
-
},
|
|
1457
|
-
"Email send"
|
|
1458
|
-
);
|
|
1459
|
-
ctx.out.outro(`${template} \u2192 ${statusColor2(res.status)}.`);
|
|
1460
|
-
}
|
|
1461
|
-
async function run5(ctx) {
|
|
1462
|
-
const sub = ctx.argv[0];
|
|
1463
|
-
switch (sub) {
|
|
1464
|
-
case "send":
|
|
1465
|
-
return runSend2(ctx, ctx.argv.slice(1));
|
|
1466
|
-
case void 0:
|
|
1467
|
-
ctx.out.fail(
|
|
1468
|
-
"emails requires a subcommand: send (see hogsend emails --help)"
|
|
1469
|
-
);
|
|
1470
|
-
break;
|
|
1471
|
-
default:
|
|
1472
|
-
ctx.out.fail(`unknown emails subcommand "${sub}" \u2014 expected send`);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
var emailsCommand = {
|
|
1476
|
-
name: "emails",
|
|
1477
|
-
summary: "Send a transactional email through the data plane",
|
|
1478
|
-
usage: usage5,
|
|
1479
|
-
run: run5
|
|
1480
|
-
};
|
|
1481
|
-
|
|
1482
|
-
// src/commands/events.ts
|
|
1483
|
-
import { parseArgs as parseArgs6 } from "util";
|
|
1484
|
-
var usage6 = `hogsend events <userId> [options]
|
|
1485
|
-
hogsend events send <name> [options]
|
|
1486
|
-
|
|
1487
|
-
Read a single user's event history (admin API), or send an event into the data
|
|
1488
|
-
plane to drive journeys/buckets.
|
|
1489
|
-
|
|
1490
|
-
Read mode \u2014 hogsend events <userId>:
|
|
1491
|
-
Stream the event history for a single user, newest first. Wraps
|
|
1492
|
-
GET /v1/admin/events?userId=<userId>.
|
|
1493
|
-
|
|
1494
|
-
Arguments:
|
|
1495
|
-
<userId> The user (distinct) id to fetch events for. Required.
|
|
1496
|
-
|
|
1497
|
-
Options:
|
|
1498
|
-
--event <name> Filter to a single event name.
|
|
1499
|
-
--from <iso> Only events at/after this ISO-8601 timestamp.
|
|
1500
|
-
--to <iso> Only events at/before this ISO-8601 timestamp.
|
|
1501
|
-
--limit <n> Max events to return (1-100, default 50).
|
|
1502
|
-
--offset <n> Pagination offset (default 0).
|
|
1503
|
-
|
|
1504
|
-
Send mode \u2014 hogsend events send <name>:
|
|
1505
|
-
Push an event into POST /v1/events (data plane, ingest key). At least one of
|
|
1506
|
-
--email / --user-id is required.
|
|
1507
|
-
|
|
1508
|
-
Options:
|
|
1509
|
-
--email <addr> Recipient/identity email.
|
|
1510
|
-
--user-id <id> External (distinct) id.
|
|
1511
|
-
--prop <key=value> Event property; repeatable. Value parsed as JSON,
|
|
1512
|
-
falling back to a string.
|
|
1513
|
-
--props <json> Event properties as one JSON object.
|
|
1514
|
-
--contact-prop <k=v> Contact property to merge onto the contact; repeatable.
|
|
1515
|
-
--contact-props <json> Contact properties as one JSON object.
|
|
1516
|
-
--list <id> Subscribe to a list; repeatable.
|
|
1517
|
-
--unlist <id> Unsubscribe from a list; repeatable.
|
|
1518
|
-
--idempotency-key <k> Dedup key (sent as the Idempotency-Key header).
|
|
1519
|
-
--timestamp <iso> Override the event timestamp.
|
|
1520
|
-
|
|
1521
|
-
Global options (handled by the router): --url, --admin-key, --data-key, --json,
|
|
1522
|
-
-h/--help.
|
|
1523
|
-
|
|
1524
|
-
Examples:
|
|
1525
|
-
hogsend events user_123
|
|
1526
|
-
hogsend events user_123 --event signup --limit 10
|
|
1527
|
-
hogsend events user_123 --from 2026-01-01T00:00:00Z --json
|
|
1528
|
-
hogsend events send signup --user-id user_123 --prop plan=pro
|
|
1529
|
-
hogsend events send purchase --email a@b.com --props '{"amount":49}' --json`;
|
|
1530
|
-
async function run6(ctx) {
|
|
1531
|
-
if (ctx.argv[0] === "send") {
|
|
1532
|
-
return runSend3(ctx, ctx.argv.slice(1));
|
|
1533
|
-
}
|
|
1534
|
-
return runRead(ctx, ctx.argv);
|
|
1535
|
-
}
|
|
1536
|
-
async function runRead(ctx, argv) {
|
|
1537
|
-
const { values: values2, positionals } = parseArgs6({
|
|
1538
|
-
args: argv,
|
|
1539
|
-
allowPositionals: true,
|
|
1540
|
-
options: {
|
|
1541
|
-
event: { type: "string" },
|
|
1542
|
-
from: { type: "string" },
|
|
1543
|
-
to: { type: "string" },
|
|
1544
|
-
limit: { type: "string" },
|
|
1545
|
-
offset: { type: "string" },
|
|
1546
|
-
help: { type: "boolean", short: "h", default: false }
|
|
2984
|
+
} catch {
|
|
2985
|
+
ctx.out.fail(`--props must be valid JSON, got: ${propsJson}`);
|
|
1547
2986
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
const userId = positionals[0];
|
|
1554
|
-
if (!userId) {
|
|
1555
|
-
ctx.out.fail("events requires a userId, e.g. hogsend events user_123");
|
|
2987
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2988
|
+
ctx.out.fail("--props must be a JSON object");
|
|
2989
|
+
}
|
|
2990
|
+
Object.assign(out, parsed);
|
|
2991
|
+
any = true;
|
|
1556
2992
|
}
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
event: values2.event,
|
|
1562
|
-
from: values2.from,
|
|
1563
|
-
to: values2.to,
|
|
1564
|
-
limit,
|
|
1565
|
-
offset
|
|
1566
|
-
};
|
|
1567
|
-
let data;
|
|
1568
|
-
try {
|
|
1569
|
-
data = await ctx.out.step(
|
|
1570
|
-
`Fetching events for ${userId}`,
|
|
1571
|
-
() => ctx.http.get("/v1/admin/events", query)
|
|
1572
|
-
);
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
if (isHttpError(error)) {
|
|
1575
|
-
ctx.out.fail(error.message);
|
|
2993
|
+
for (const pair of propPairs ?? []) {
|
|
2994
|
+
const eq2 = pair.indexOf("=");
|
|
2995
|
+
if (eq2 === -1) {
|
|
2996
|
+
ctx.out.fail(`--prop must be key=value, got: ${pair}`);
|
|
1576
2997
|
}
|
|
1577
|
-
|
|
2998
|
+
const key = pair.slice(0, eq2).trim();
|
|
2999
|
+
if (key === "") {
|
|
3000
|
+
ctx.out.fail(`--prop key cannot be empty, got: ${pair}`);
|
|
3001
|
+
}
|
|
3002
|
+
out[key] = coerceValue4(pair.slice(eq2 + 1));
|
|
3003
|
+
any = true;
|
|
1578
3004
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
3005
|
+
return any ? out : void 0;
|
|
3006
|
+
}
|
|
3007
|
+
function coerceValue4(raw) {
|
|
3008
|
+
try {
|
|
3009
|
+
return JSON.parse(raw);
|
|
3010
|
+
} catch {
|
|
3011
|
+
return raw;
|
|
1582
3012
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
3013
|
+
}
|
|
3014
|
+
function statusColor2(status) {
|
|
3015
|
+
switch (status) {
|
|
3016
|
+
case "queued":
|
|
3017
|
+
case "sent":
|
|
3018
|
+
return color.green(status);
|
|
3019
|
+
case "skipped":
|
|
3020
|
+
return color.dim(status);
|
|
3021
|
+
default:
|
|
3022
|
+
return color.yellow(status);
|
|
1591
3023
|
}
|
|
1592
|
-
const rows = data.events.map((e) => ({
|
|
1593
|
-
occurredAt: e.occurredAt,
|
|
1594
|
-
event: e.event,
|
|
1595
|
-
properties: summarizeProps(e.properties),
|
|
1596
|
-
id: e.id
|
|
1597
|
-
}));
|
|
1598
|
-
ctx.out.table(rows, ["occurredAt", "event", "properties", "id"]);
|
|
1599
|
-
const shown = data.events.length;
|
|
1600
|
-
const through = data.offset + shown;
|
|
1601
|
-
ctx.out.outro(
|
|
1602
|
-
`${color.green(String(shown))} event${shown === 1 ? "" : "s"} ` + color.dim(`(${data.offset + 1}-${through} of ${data.total})`)
|
|
1603
|
-
);
|
|
1604
3024
|
}
|
|
1605
3025
|
async function runSend3(ctx, argv) {
|
|
1606
|
-
const { values: values2, positionals } =
|
|
3026
|
+
const { values: values2, positionals } = parseArgs9({
|
|
1607
3027
|
args: argv,
|
|
1608
3028
|
allowPositionals: true,
|
|
1609
3029
|
options: {
|
|
1610
|
-
|
|
3030
|
+
to: { type: "string" },
|
|
1611
3031
|
"user-id": { type: "string" },
|
|
1612
3032
|
prop: { type: "string", multiple: true },
|
|
1613
3033
|
props: { type: "string" },
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
3034
|
+
from: { type: "string" },
|
|
3035
|
+
subject: { type: "string" },
|
|
3036
|
+
"reply-to": { type: "string" },
|
|
3037
|
+
category: { type: "string" },
|
|
3038
|
+
"skip-preference-check": { type: "boolean", default: false },
|
|
1618
3039
|
"idempotency-key": { type: "string" },
|
|
1619
|
-
timestamp: { type: "string" },
|
|
1620
3040
|
help: { type: "boolean", short: "h", default: false }
|
|
1621
3041
|
}
|
|
1622
3042
|
});
|
|
1623
3043
|
if (values2.help) {
|
|
1624
|
-
ctx.out.log(
|
|
3044
|
+
ctx.out.log(usage8);
|
|
1625
3045
|
return;
|
|
1626
3046
|
}
|
|
1627
|
-
const
|
|
1628
|
-
if (!
|
|
3047
|
+
const template = positionals[0];
|
|
3048
|
+
if (!template) {
|
|
1629
3049
|
ctx.out.fail(
|
|
1630
|
-
"
|
|
3050
|
+
"emails send requires a template, e.g. hogsend emails send welcome --to a@b.com"
|
|
1631
3051
|
);
|
|
1632
3052
|
}
|
|
1633
|
-
const
|
|
3053
|
+
const to = values2.to;
|
|
1634
3054
|
const userId = values2["user-id"];
|
|
1635
|
-
if (!
|
|
1636
|
-
ctx.out.fail("
|
|
3055
|
+
if (!to && !userId) {
|
|
3056
|
+
ctx.out.fail("emails send requires at least one of --to or --user-id");
|
|
1637
3057
|
}
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
values2["contact-props"],
|
|
1642
|
-
values2["contact-prop"],
|
|
1643
|
-
"contact-prop"
|
|
1644
|
-
);
|
|
1645
|
-
const lists = parseLists2(values2.list, values2.unlist);
|
|
1646
|
-
const body = { name };
|
|
1647
|
-
if (email) body.email = email;
|
|
3058
|
+
const props = parseProps4(ctx, values2.props, values2.prop);
|
|
3059
|
+
const body = { template };
|
|
3060
|
+
if (to) body.to = to;
|
|
1648
3061
|
if (userId) body.userId = userId;
|
|
1649
|
-
if (
|
|
1650
|
-
if (
|
|
1651
|
-
if (
|
|
3062
|
+
if (props) body.props = props;
|
|
3063
|
+
if (values2.from) body.from = values2.from;
|
|
3064
|
+
if (values2.subject) body.subject = values2.subject;
|
|
3065
|
+
if (values2["reply-to"]) body.replyTo = values2["reply-to"];
|
|
3066
|
+
if (values2.category) body.category = values2.category;
|
|
3067
|
+
if (values2["skip-preference-check"]) body.skipPreferenceCheck = true;
|
|
1652
3068
|
if (values2["idempotency-key"]) {
|
|
1653
3069
|
body.idempotencyKey = values2["idempotency-key"];
|
|
1654
3070
|
}
|
|
1655
|
-
if (values2.timestamp) body.timestamp = values2.timestamp;
|
|
1656
3071
|
let res;
|
|
1657
3072
|
try {
|
|
1658
3073
|
res = await ctx.out.step(
|
|
1659
|
-
`Sending
|
|
1660
|
-
() => ctx.dataHttp.post("/v1/
|
|
3074
|
+
`Sending ${template}`,
|
|
3075
|
+
() => ctx.dataHttp.post("/v1/emails", body)
|
|
1661
3076
|
);
|
|
1662
3077
|
} catch (error) {
|
|
1663
3078
|
if (isHttpError(error)) {
|
|
@@ -1669,103 +3084,43 @@ async function runSend3(ctx, argv) {
|
|
|
1669
3084
|
ctx.out.json(res);
|
|
1670
3085
|
return;
|
|
1671
3086
|
}
|
|
1672
|
-
ctx.out.intro(`${
|
|
1673
|
-
const exited = res.exits.filter((e) => e.exited);
|
|
3087
|
+
ctx.out.intro(`${badge4} send`);
|
|
1674
3088
|
ctx.out.kv(
|
|
1675
3089
|
{
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
3090
|
+
emailSendId: res.emailSendId,
|
|
3091
|
+
template,
|
|
3092
|
+
recipient: to ?? userId ?? "",
|
|
3093
|
+
status: statusColor2(res.status),
|
|
3094
|
+
reason: res.reason ?? ""
|
|
1681
3095
|
},
|
|
1682
|
-
"
|
|
1683
|
-
);
|
|
1684
|
-
if (exited.length > 0) {
|
|
1685
|
-
ctx.out.table(
|
|
1686
|
-
exited.map((e) => ({ journeyId: e.journeyId, stateId: e.stateId })),
|
|
1687
|
-
["journeyId", "stateId"]
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
ctx.out.outro(
|
|
1691
|
-
res.stored ? `${color.green("Stored")} ${name}.` : color.dim(`${name} was deduped (not stored).`)
|
|
3096
|
+
"Email send"
|
|
1692
3097
|
);
|
|
3098
|
+
ctx.out.outro(`${template} \u2192 ${statusColor2(res.status)}.`);
|
|
1693
3099
|
}
|
|
1694
|
-
function
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
ctx.out.fail(
|
|
1706
|
-
}
|
|
1707
|
-
Object.assign(out, parsed);
|
|
1708
|
-
any = true;
|
|
1709
|
-
}
|
|
1710
|
-
for (const pair of pairs ?? []) {
|
|
1711
|
-
const eq2 = pair.indexOf("=");
|
|
1712
|
-
if (eq2 === -1) {
|
|
1713
|
-
ctx.out.fail(`--${flagName} must be key=value, got: ${pair}`);
|
|
1714
|
-
}
|
|
1715
|
-
const key = pair.slice(0, eq2).trim();
|
|
1716
|
-
if (key === "") {
|
|
1717
|
-
ctx.out.fail(`--${flagName} key cannot be empty, got: ${pair}`);
|
|
1718
|
-
}
|
|
1719
|
-
out[key] = coerceValue4(pair.slice(eq2 + 1));
|
|
1720
|
-
any = true;
|
|
1721
|
-
}
|
|
1722
|
-
return any ? out : void 0;
|
|
1723
|
-
}
|
|
1724
|
-
function coerceValue4(raw) {
|
|
1725
|
-
try {
|
|
1726
|
-
return JSON.parse(raw);
|
|
1727
|
-
} catch {
|
|
1728
|
-
return raw;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
function parseLists2(subscribe, unsubscribe) {
|
|
1732
|
-
const out = {};
|
|
1733
|
-
let any = false;
|
|
1734
|
-
for (const id of subscribe ?? []) {
|
|
1735
|
-
out[id] = true;
|
|
1736
|
-
any = true;
|
|
1737
|
-
}
|
|
1738
|
-
for (const id of unsubscribe ?? []) {
|
|
1739
|
-
out[id] = false;
|
|
1740
|
-
any = true;
|
|
1741
|
-
}
|
|
1742
|
-
return any ? out : void 0;
|
|
1743
|
-
}
|
|
1744
|
-
function parseNumber(raw, name, ctx) {
|
|
1745
|
-
if (raw === void 0) return void 0;
|
|
1746
|
-
const n = Number(raw);
|
|
1747
|
-
if (!Number.isFinite(n)) {
|
|
1748
|
-
ctx.out.fail(`--${name} must be a number, got "${raw}"`);
|
|
3100
|
+
async function run8(ctx) {
|
|
3101
|
+
const sub = ctx.argv[0];
|
|
3102
|
+
switch (sub) {
|
|
3103
|
+
case "send":
|
|
3104
|
+
return runSend3(ctx, ctx.argv.slice(1));
|
|
3105
|
+
case void 0:
|
|
3106
|
+
ctx.out.fail(
|
|
3107
|
+
"emails requires a subcommand: send (see hogsend emails --help)"
|
|
3108
|
+
);
|
|
3109
|
+
break;
|
|
3110
|
+
default:
|
|
3111
|
+
ctx.out.fail(`unknown emails subcommand "${sub}" \u2014 expected send`);
|
|
1749
3112
|
}
|
|
1750
|
-
return n;
|
|
1751
|
-
}
|
|
1752
|
-
function summarizeProps(props) {
|
|
1753
|
-
if (!props) return "";
|
|
1754
|
-
const keys = Object.keys(props);
|
|
1755
|
-
if (keys.length === 0) return "";
|
|
1756
|
-
const preview = JSON.stringify(props);
|
|
1757
|
-
return preview.length > 60 ? `${preview.slice(0, 57)}...` : preview;
|
|
1758
3113
|
}
|
|
1759
|
-
var
|
|
1760
|
-
name: "
|
|
1761
|
-
summary: "
|
|
1762
|
-
usage:
|
|
1763
|
-
run:
|
|
3114
|
+
var emailsCommand = {
|
|
3115
|
+
name: "emails",
|
|
3116
|
+
summary: "Send a transactional email through the data plane",
|
|
3117
|
+
usage: usage8,
|
|
3118
|
+
run: run8
|
|
1764
3119
|
};
|
|
1765
3120
|
|
|
1766
3121
|
// src/commands/journeys.ts
|
|
1767
|
-
import { parseArgs as
|
|
1768
|
-
var
|
|
3122
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
3123
|
+
var usage9 = `hogsend journeys <subcommand> [options]
|
|
1769
3124
|
|
|
1770
3125
|
Inspect and toggle journeys via the admin API (/v1/admin/journeys).
|
|
1771
3126
|
|
|
@@ -1787,14 +3142,14 @@ Examples:
|
|
|
1787
3142
|
hogsend journeys list --enabled true
|
|
1788
3143
|
hogsend journeys get activation-welcome --json
|
|
1789
3144
|
hogsend journeys disable churn-prevention`;
|
|
1790
|
-
function
|
|
3145
|
+
function badge5() {
|
|
1791
3146
|
return `${color.bgMagenta(color.black(" hogsend "))} journeys`;
|
|
1792
3147
|
}
|
|
1793
3148
|
function statusColor3(enabled) {
|
|
1794
3149
|
return enabled ? color.green("enabled") : color.yellow("disabled");
|
|
1795
3150
|
}
|
|
1796
3151
|
async function runList2(ctx) {
|
|
1797
|
-
const { values: values2 } =
|
|
3152
|
+
const { values: values2 } = parseArgs10({
|
|
1798
3153
|
args: ctx.argv,
|
|
1799
3154
|
allowPositionals: true,
|
|
1800
3155
|
options: {
|
|
@@ -1805,7 +3160,7 @@ async function runList2(ctx) {
|
|
|
1805
3160
|
}
|
|
1806
3161
|
});
|
|
1807
3162
|
if (values2.help) {
|
|
1808
|
-
ctx.out.log(
|
|
3163
|
+
ctx.out.log(usage9);
|
|
1809
3164
|
return;
|
|
1810
3165
|
}
|
|
1811
3166
|
if (values2.enabled !== void 0 && !["true", "false"].includes(values2.enabled)) {
|
|
@@ -1816,7 +3171,7 @@ async function runList2(ctx) {
|
|
|
1816
3171
|
limit: values2.limit,
|
|
1817
3172
|
offset: values2.offset
|
|
1818
3173
|
};
|
|
1819
|
-
if (!ctx.json) ctx.out.intro(
|
|
3174
|
+
if (!ctx.json) ctx.out.intro(badge5());
|
|
1820
3175
|
const data = await ctx.out.step(
|
|
1821
3176
|
"Fetching journeys",
|
|
1822
3177
|
() => ctx.http.get("/v1/admin/journeys", query)
|
|
@@ -1861,7 +3216,7 @@ async function runGet2(ctx, id) {
|
|
|
1861
3216
|
"journeys get requires a journey id, e.g. hogsend journeys get activation-welcome"
|
|
1862
3217
|
);
|
|
1863
3218
|
}
|
|
1864
|
-
if (!ctx.json) ctx.out.intro(
|
|
3219
|
+
if (!ctx.json) ctx.out.intro(badge5());
|
|
1865
3220
|
const data = await ctx.out.step(
|
|
1866
3221
|
`Fetching journey ${id}`,
|
|
1867
3222
|
() => ctx.http.get(
|
|
@@ -1918,7 +3273,7 @@ async function runToggle(ctx, id, enabled) {
|
|
|
1918
3273
|
`journeys ${verb} requires a journey id, e.g. hogsend journeys ${verb} activation-welcome`
|
|
1919
3274
|
);
|
|
1920
3275
|
}
|
|
1921
|
-
if (!ctx.json) ctx.out.intro(
|
|
3276
|
+
if (!ctx.json) ctx.out.intro(badge5());
|
|
1922
3277
|
const data = await ctx.out.step(
|
|
1923
3278
|
`${enabled ? "Enabling" : "Disabling"} ${id}`,
|
|
1924
3279
|
() => ctx.http.patch(
|
|
@@ -1941,7 +3296,7 @@ async function runToggle(ctx, id, enabled) {
|
|
|
1941
3296
|
);
|
|
1942
3297
|
ctx.out.outro(`${j.id} is now ${statusColor3(j.enabled)}.`);
|
|
1943
3298
|
}
|
|
1944
|
-
async function
|
|
3299
|
+
async function run9(ctx) {
|
|
1945
3300
|
const sub = ctx.argv[0];
|
|
1946
3301
|
const rest = ctx.argv.slice(1);
|
|
1947
3302
|
const subCtx = { ...ctx, argv: rest };
|
|
@@ -1953,7 +3308,7 @@ async function run7(ctx) {
|
|
|
1953
3308
|
case "get": {
|
|
1954
3309
|
const id = rest.find((a) => !a.startsWith("-"));
|
|
1955
3310
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1956
|
-
ctx.out.log(
|
|
3311
|
+
ctx.out.log(usage9);
|
|
1957
3312
|
return;
|
|
1958
3313
|
}
|
|
1959
3314
|
await runGet2(subCtx, id);
|
|
@@ -1961,7 +3316,7 @@ async function run7(ctx) {
|
|
|
1961
3316
|
}
|
|
1962
3317
|
case "enable": {
|
|
1963
3318
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1964
|
-
ctx.out.log(
|
|
3319
|
+
ctx.out.log(usage9);
|
|
1965
3320
|
return;
|
|
1966
3321
|
}
|
|
1967
3322
|
await runToggle(
|
|
@@ -1973,7 +3328,7 @@ async function run7(ctx) {
|
|
|
1973
3328
|
}
|
|
1974
3329
|
case "disable": {
|
|
1975
3330
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1976
|
-
ctx.out.log(
|
|
3331
|
+
ctx.out.log(usage9);
|
|
1977
3332
|
return;
|
|
1978
3333
|
}
|
|
1979
3334
|
await runToggle(
|
|
@@ -2007,14 +3362,14 @@ async function run7(ctx) {
|
|
|
2007
3362
|
var journeysCommand = {
|
|
2008
3363
|
name: "journeys",
|
|
2009
3364
|
summary: "List, inspect, enable, and disable journeys",
|
|
2010
|
-
usage:
|
|
2011
|
-
run:
|
|
3365
|
+
usage: usage9,
|
|
3366
|
+
run: run9
|
|
2012
3367
|
};
|
|
2013
3368
|
|
|
2014
|
-
// src/commands/patch.ts
|
|
2015
|
-
import { spawnSync } from "child_process";
|
|
2016
|
-
import { parseArgs as
|
|
2017
|
-
var
|
|
3369
|
+
// src/commands/patch.ts
|
|
3370
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3371
|
+
import { parseArgs as parseArgs11 } from "util";
|
|
3372
|
+
var usage10 = `hogsend patch <package> [--cwd <dir>]
|
|
2018
3373
|
|
|
2019
3374
|
Thin wrapper over pnpm's native patch flow. Runs \`pnpm patch <package>\`, which
|
|
2020
3375
|
extracts the package into a temp dir and prints the path to edit. After editing,
|
|
@@ -2025,8 +3380,8 @@ This does NOT replace scripts/patch-check.sh (the patch re-apply contract).
|
|
|
2025
3380
|
Options:
|
|
2026
3381
|
--cwd <dir> Project root to run pnpm in (defaults to current directory).
|
|
2027
3382
|
-h, --help Show this help.`;
|
|
2028
|
-
async function
|
|
2029
|
-
const { values: values2, positionals } =
|
|
3383
|
+
async function run10(ctx) {
|
|
3384
|
+
const { values: values2, positionals } = parseArgs11({
|
|
2030
3385
|
args: ctx.argv,
|
|
2031
3386
|
allowPositionals: true,
|
|
2032
3387
|
options: {
|
|
@@ -2035,7 +3390,7 @@ async function run8(ctx) {
|
|
|
2035
3390
|
}
|
|
2036
3391
|
});
|
|
2037
3392
|
if (values2.help) {
|
|
2038
|
-
ctx.out.log(
|
|
3393
|
+
ctx.out.log(usage10);
|
|
2039
3394
|
return;
|
|
2040
3395
|
}
|
|
2041
3396
|
const pkg = positionals[0];
|
|
@@ -2045,7 +3400,7 @@ async function run8(ctx) {
|
|
|
2045
3400
|
);
|
|
2046
3401
|
}
|
|
2047
3402
|
const cwd = values2.cwd ?? process.cwd();
|
|
2048
|
-
const result =
|
|
3403
|
+
const result = spawnSync3("pnpm", ["patch", pkg], {
|
|
2049
3404
|
cwd,
|
|
2050
3405
|
stdio: ctx.json ? "ignore" : "inherit"
|
|
2051
3406
|
});
|
|
@@ -2075,30 +3430,16 @@ async function run8(ctx) {
|
|
|
2075
3430
|
var patchCommand = {
|
|
2076
3431
|
name: "patch",
|
|
2077
3432
|
summary: "Patch a package via pnpm's native patch flow",
|
|
2078
|
-
usage:
|
|
2079
|
-
run:
|
|
3433
|
+
usage: usage10,
|
|
3434
|
+
run: run10
|
|
2080
3435
|
};
|
|
2081
3436
|
|
|
2082
3437
|
// src/commands/setup.ts
|
|
2083
|
-
import {
|
|
2084
|
-
import {
|
|
2085
|
-
import {
|
|
2086
|
-
import {
|
|
2087
|
-
|
|
2088
|
-
import { confirm } from "@clack/prompts";
|
|
2089
|
-
|
|
2090
|
-
// src/lib/prompt.ts
|
|
2091
|
-
import { cancel as cancel2, isCancel } from "@clack/prompts";
|
|
2092
|
-
function bail(value) {
|
|
2093
|
-
if (isCancel(value)) {
|
|
2094
|
-
cancel2("Cancelled.");
|
|
2095
|
-
process.exit(0);
|
|
2096
|
-
}
|
|
2097
|
-
return value;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
// src/commands/setup.ts
|
|
2101
|
-
var usage9 = `hogsend setup [--cwd <dir>] [--yes] [--json]
|
|
3438
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3439
|
+
import { join as join7 } from "path";
|
|
3440
|
+
import { parseArgs as parseArgs12 } from "util";
|
|
3441
|
+
import { confirm as confirm2 } from "@clack/prompts";
|
|
3442
|
+
var usage11 = `hogsend setup [--cwd <dir>] [--yes] [--json]
|
|
2102
3443
|
|
|
2103
3444
|
Interactive local onboarding for a scaffolded Hogsend app. Mirrors the
|
|
2104
3445
|
create-hogsend "next steps":
|
|
@@ -2115,102 +3456,8 @@ Options:
|
|
|
2115
3456
|
-h, --help Show this help.
|
|
2116
3457
|
|
|
2117
3458
|
Run ${color.cyan("hogsend doctor")} afterwards to verify the instance is healthy.`;
|
|
2118
|
-
function
|
|
2119
|
-
|
|
2120
|
-
}
|
|
2121
|
-
var SECRET_KEY = "BETTER_AUTH_SECRET";
|
|
2122
|
-
var PLACEHOLDER_PREFIX = "change-me";
|
|
2123
|
-
function ensureEnv(cwd) {
|
|
2124
|
-
const envPath = join4(cwd, ".env");
|
|
2125
|
-
const examplePath = join4(cwd, ".env.example");
|
|
2126
|
-
let copied;
|
|
2127
|
-
if (existsSync4(envPath)) {
|
|
2128
|
-
copied = {
|
|
2129
|
-
step: "env",
|
|
2130
|
-
status: "skipped",
|
|
2131
|
-
detail: ".env already exists"
|
|
2132
|
-
};
|
|
2133
|
-
} else if (existsSync4(examplePath)) {
|
|
2134
|
-
copyFileSync(examplePath, envPath);
|
|
2135
|
-
copied = {
|
|
2136
|
-
step: "env",
|
|
2137
|
-
status: "ok",
|
|
2138
|
-
detail: "copied .env.example -> .env"
|
|
2139
|
-
};
|
|
2140
|
-
} else {
|
|
2141
|
-
copied = {
|
|
2142
|
-
step: "env",
|
|
2143
|
-
status: "failed",
|
|
2144
|
-
detail: "no .env and no .env.example to copy from"
|
|
2145
|
-
};
|
|
2146
|
-
return {
|
|
2147
|
-
copied,
|
|
2148
|
-
secret: {
|
|
2149
|
-
step: "secret",
|
|
2150
|
-
status: "skipped",
|
|
2151
|
-
detail: "skipped \u2014 no .env"
|
|
2152
|
-
}
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2155
|
-
let raw;
|
|
2156
|
-
try {
|
|
2157
|
-
raw = readFileSync2(envPath, "utf8");
|
|
2158
|
-
} catch (err) {
|
|
2159
|
-
return {
|
|
2160
|
-
copied,
|
|
2161
|
-
secret: {
|
|
2162
|
-
step: "secret",
|
|
2163
|
-
status: "failed",
|
|
2164
|
-
detail: `could not read .env: ${err instanceof Error ? err.message : String(err)}`
|
|
2165
|
-
}
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
const lines = raw.split(/\r?\n/);
|
|
2169
|
-
const idx = lines.findIndex(
|
|
2170
|
-
(l) => l.replace(/^export\s+/, "").trimStart().startsWith(`${SECRET_KEY}=`)
|
|
2171
|
-
);
|
|
2172
|
-
const existingLine = idx === -1 ? void 0 : lines[idx];
|
|
2173
|
-
const current = existingLine === void 0 ? void 0 : existingLine.slice(existingLine.indexOf("=") + 1).trim();
|
|
2174
|
-
const isPlaceholder = current === void 0 || current === "" || current.startsWith(PLACEHOLDER_PREFIX);
|
|
2175
|
-
if (!isPlaceholder) {
|
|
2176
|
-
return {
|
|
2177
|
-
copied,
|
|
2178
|
-
secret: {
|
|
2179
|
-
step: "secret",
|
|
2180
|
-
status: "skipped",
|
|
2181
|
-
detail: `${SECRET_KEY} already set`
|
|
2182
|
-
}
|
|
2183
|
-
};
|
|
2184
|
-
}
|
|
2185
|
-
const secret = generateSecret();
|
|
2186
|
-
const newLine = `${SECRET_KEY}=${secret}`;
|
|
2187
|
-
if (idx === -1) {
|
|
2188
|
-
if (raw.length > 0 && !raw.endsWith("\n")) lines.push("");
|
|
2189
|
-
lines.push(newLine);
|
|
2190
|
-
} else {
|
|
2191
|
-
lines[idx] = newLine;
|
|
2192
|
-
}
|
|
2193
|
-
writeFileSync2(envPath, lines.join("\n"));
|
|
2194
|
-
return {
|
|
2195
|
-
copied,
|
|
2196
|
-
secret: {
|
|
2197
|
-
step: "secret",
|
|
2198
|
-
status: "ok",
|
|
2199
|
-
detail: `generated ${SECRET_KEY} (64-char hex)`
|
|
2200
|
-
}
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
function runCmd(cmd, args, cwd, json2) {
|
|
2204
|
-
const result = spawnSync2(cmd, args, {
|
|
2205
|
-
cwd,
|
|
2206
|
-
// In json mode stay silent (we report structured status); otherwise stream
|
|
2207
|
-
// so the user sees docker / migration output inline.
|
|
2208
|
-
stdio: json2 ? "ignore" : "inherit"
|
|
2209
|
-
});
|
|
2210
|
-
return { status: result.status, ok: result.status === 0 };
|
|
2211
|
-
}
|
|
2212
|
-
async function run9(ctx) {
|
|
2213
|
-
const { values: values2 } = parseArgs9({
|
|
3459
|
+
async function run11(ctx) {
|
|
3460
|
+
const { values: values2 } = parseArgs12({
|
|
2214
3461
|
args: ctx.argv,
|
|
2215
3462
|
allowPositionals: true,
|
|
2216
3463
|
options: {
|
|
@@ -2220,16 +3467,16 @@ async function run9(ctx) {
|
|
|
2220
3467
|
}
|
|
2221
3468
|
});
|
|
2222
3469
|
if (values2.help) {
|
|
2223
|
-
ctx.out.log(
|
|
3470
|
+
ctx.out.log(usage11);
|
|
2224
3471
|
return;
|
|
2225
3472
|
}
|
|
2226
3473
|
const cwd = values2.cwd ?? process.cwd();
|
|
2227
|
-
if (!
|
|
3474
|
+
if (!existsSync7(join7(cwd, "package.json"))) {
|
|
2228
3475
|
ctx.out.fail(
|
|
2229
3476
|
`no package.json in ${cwd} \u2014 run setup from a scaffolded Hogsend app (or pass --cwd).`
|
|
2230
3477
|
);
|
|
2231
3478
|
}
|
|
2232
|
-
const hasCompose =
|
|
3479
|
+
const hasCompose = hasComposeFile(cwd);
|
|
2233
3480
|
const skipConfirm = ctx.json || values2.yes;
|
|
2234
3481
|
if (!ctx.json) {
|
|
2235
3482
|
ctx.out.intro(
|
|
@@ -2238,7 +3485,7 @@ async function run9(ctx) {
|
|
|
2238
3485
|
}
|
|
2239
3486
|
if (ctx.out.interactive && !skipConfirm) {
|
|
2240
3487
|
const proceed = bail(
|
|
2241
|
-
await
|
|
3488
|
+
await confirm2({
|
|
2242
3489
|
message: `Set up local infra in ${color.cyan(cwd)}? (docker compose up, .env, db:migrate)`
|
|
2243
3490
|
})
|
|
2244
3491
|
);
|
|
@@ -2249,15 +3496,23 @@ async function run9(ctx) {
|
|
|
2249
3496
|
}
|
|
2250
3497
|
const results = [];
|
|
2251
3498
|
if (hasCompose) {
|
|
2252
|
-
const
|
|
2253
|
-
"
|
|
2254
|
-
async () =>
|
|
3499
|
+
const infra = await ctx.out.step(
|
|
3500
|
+
"Checking infra",
|
|
3501
|
+
async () => detectRunningInfra(cwd)
|
|
2255
3502
|
);
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
3503
|
+
if (infra.postgres && infra.redis && infra.hatchet) {
|
|
3504
|
+
results.push({
|
|
3505
|
+
step: "docker",
|
|
3506
|
+
status: "skipped",
|
|
3507
|
+
detail: "infra already running"
|
|
3508
|
+
});
|
|
3509
|
+
} else {
|
|
3510
|
+
const docker = await ctx.out.step(
|
|
3511
|
+
"Starting infra (docker compose up -d)",
|
|
3512
|
+
async () => dockerComposeUp(cwd, { quiet: ctx.json })
|
|
3513
|
+
);
|
|
3514
|
+
results.push(docker);
|
|
3515
|
+
}
|
|
2261
3516
|
} else {
|
|
2262
3517
|
results.push({
|
|
2263
3518
|
step: "docker",
|
|
@@ -2265,10 +3520,10 @@ async function run9(ctx) {
|
|
|
2265
3520
|
detail: "no docker-compose file found"
|
|
2266
3521
|
});
|
|
2267
3522
|
}
|
|
2268
|
-
const env = await ctx.out.step(
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
);
|
|
3523
|
+
const env = await ctx.out.step("Preparing .env + auth secret", async () => ({
|
|
3524
|
+
copied: ensureEnvFile(cwd),
|
|
3525
|
+
secret: ensureAuthSecret(cwd)
|
|
3526
|
+
}));
|
|
2272
3527
|
results.push(env.copied, env.secret);
|
|
2273
3528
|
const dockerFailed = results.some(
|
|
2274
3529
|
(r) => r.step === "docker" && r.status === "failed"
|
|
@@ -2282,13 +3537,9 @@ async function run9(ctx) {
|
|
|
2282
3537
|
} else {
|
|
2283
3538
|
const migrate = await ctx.out.step(
|
|
2284
3539
|
"Running migrations (pnpm db:migrate)",
|
|
2285
|
-
async () =>
|
|
3540
|
+
async () => runMigrations(cwd, { quiet: ctx.json })
|
|
2286
3541
|
);
|
|
2287
|
-
results.push(
|
|
2288
|
-
step: "migrate",
|
|
2289
|
-
status: migrate.ok ? "ok" : "failed",
|
|
2290
|
-
detail: migrate.ok ? "engine + client migrations applied" : `pnpm db:migrate exited with code ${migrate.status ?? "?"}`
|
|
2291
|
-
});
|
|
3542
|
+
results.push(migrate);
|
|
2292
3543
|
}
|
|
2293
3544
|
const failed = results.filter((r) => r.status === "failed");
|
|
2294
3545
|
const ok = failed.length === 0;
|
|
@@ -2331,16 +3582,16 @@ async function run9(ctx) {
|
|
|
2331
3582
|
var setupCommand = {
|
|
2332
3583
|
name: "setup",
|
|
2333
3584
|
summary: "Local onboarding: docker compose up, gen secret, db:migrate",
|
|
2334
|
-
usage:
|
|
2335
|
-
run:
|
|
3585
|
+
usage: usage11,
|
|
3586
|
+
run: run11
|
|
2336
3587
|
};
|
|
2337
3588
|
|
|
2338
3589
|
// src/commands/skills.ts
|
|
2339
|
-
import { existsSync as
|
|
2340
|
-
import { join as
|
|
2341
|
-
import { parseArgs as
|
|
3590
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3591
|
+
import { join as join8 } from "path";
|
|
3592
|
+
import { parseArgs as parseArgs13 } from "util";
|
|
2342
3593
|
import { multiselect } from "@clack/prompts";
|
|
2343
|
-
var
|
|
3594
|
+
var usage12 = `hogsend skills <subcommand> [options]
|
|
2344
3595
|
|
|
2345
3596
|
Manage the Claude Code skills bundled with @hogsend/cli. Bundled skills teach
|
|
2346
3597
|
agents how to drive the hogsend CLI; \`add\` copies them into your project's
|
|
@@ -2400,8 +3651,8 @@ function runList3(ctx) {
|
|
|
2400
3651
|
`Install with ${color.cyan("hogsend skills add <name>")} (or ${color.cyan("hogsend skills add --all")}). Refresh after an engine upgrade with ${color.cyan("--force")}.`
|
|
2401
3652
|
);
|
|
2402
3653
|
}
|
|
2403
|
-
async function
|
|
2404
|
-
const { values: values2, positionals } =
|
|
3654
|
+
async function runAdd2(ctx, argv) {
|
|
3655
|
+
const { values: values2, positionals } = parseArgs13({
|
|
2405
3656
|
args: argv,
|
|
2406
3657
|
allowPositionals: true,
|
|
2407
3658
|
options: {
|
|
@@ -2411,7 +3662,7 @@ async function runAdd(ctx, argv) {
|
|
|
2411
3662
|
}
|
|
2412
3663
|
});
|
|
2413
3664
|
if (values2.help) {
|
|
2414
|
-
ctx.out.log(
|
|
3665
|
+
ctx.out.log(usage12);
|
|
2415
3666
|
return;
|
|
2416
3667
|
}
|
|
2417
3668
|
const cwd = process.cwd();
|
|
@@ -2452,7 +3703,7 @@ async function runAdd(ctx, argv) {
|
|
|
2452
3703
|
(name) => copySkill(name, cwd, force)
|
|
2453
3704
|
);
|
|
2454
3705
|
if (results.some((r) => r.installed)) {
|
|
2455
|
-
const installedNames = listBundledSkills(cwd).filter((s) =>
|
|
3706
|
+
const installedNames = listBundledSkills(cwd).filter((s) => existsSync8(join8(installDir(cwd), s.name))).map((s) => s.name);
|
|
2456
3707
|
writeSkillsStamp(cwd, installedNames);
|
|
2457
3708
|
}
|
|
2458
3709
|
if (ctx.json) {
|
|
@@ -2479,19 +3730,19 @@ async function runAdd(ctx, argv) {
|
|
|
2479
3730
|
`Installed ${installedCount} skill${installedCount === 1 ? "" : "s"}` + (skippedCount > 0 ? `, skipped ${skippedCount}.` : ".")
|
|
2480
3731
|
);
|
|
2481
3732
|
}
|
|
2482
|
-
async function
|
|
3733
|
+
async function run12(ctx) {
|
|
2483
3734
|
const sub = ctx.argv[0];
|
|
2484
3735
|
switch (sub) {
|
|
2485
3736
|
case "list":
|
|
2486
3737
|
runList3(ctx);
|
|
2487
3738
|
return;
|
|
2488
3739
|
case "add":
|
|
2489
|
-
await
|
|
3740
|
+
await runAdd2(ctx, ctx.argv.slice(1));
|
|
2490
3741
|
return;
|
|
2491
3742
|
case void 0:
|
|
2492
3743
|
case "-h":
|
|
2493
3744
|
case "--help":
|
|
2494
|
-
ctx.out.log(
|
|
3745
|
+
ctx.out.log(usage12);
|
|
2495
3746
|
return;
|
|
2496
3747
|
default:
|
|
2497
3748
|
ctx.out.fail(
|
|
@@ -2502,13 +3753,13 @@ async function run10(ctx) {
|
|
|
2502
3753
|
var skillsCommand = {
|
|
2503
3754
|
name: "skills",
|
|
2504
3755
|
summary: "List + install bundled Claude Code skills into .claude/skills",
|
|
2505
|
-
usage:
|
|
2506
|
-
run:
|
|
3756
|
+
usage: usage12,
|
|
3757
|
+
run: run12
|
|
2507
3758
|
};
|
|
2508
3759
|
|
|
2509
3760
|
// src/commands/stats.ts
|
|
2510
|
-
import { parseArgs as
|
|
2511
|
-
var
|
|
3761
|
+
import { parseArgs as parseArgs14 } from "util";
|
|
3762
|
+
var usage13 = `hogsend stats [--json]
|
|
2512
3763
|
|
|
2513
3764
|
Show system-wide overview metrics from a running Hogsend instance.
|
|
2514
3765
|
Wraps GET /v1/admin/metrics/overview.
|
|
@@ -2530,8 +3781,8 @@ Options:
|
|
|
2530
3781
|
function pct(rate) {
|
|
2531
3782
|
return `${(rate * 100).toFixed(2)}%`;
|
|
2532
3783
|
}
|
|
2533
|
-
async function
|
|
2534
|
-
const { values: values2 } =
|
|
3784
|
+
async function run13(ctx) {
|
|
3785
|
+
const { values: values2 } = parseArgs14({
|
|
2535
3786
|
args: ctx.argv,
|
|
2536
3787
|
allowPositionals: true,
|
|
2537
3788
|
options: {
|
|
@@ -2539,7 +3790,7 @@ async function run11(ctx) {
|
|
|
2539
3790
|
}
|
|
2540
3791
|
});
|
|
2541
3792
|
if (values2.help) {
|
|
2542
|
-
ctx.out.log(
|
|
3793
|
+
ctx.out.log(usage13);
|
|
2543
3794
|
return;
|
|
2544
3795
|
}
|
|
2545
3796
|
const metrics = await ctx.out.step(
|
|
@@ -2568,20 +3819,20 @@ async function run11(ctx) {
|
|
|
2568
3819
|
var statsCommand = {
|
|
2569
3820
|
name: "stats",
|
|
2570
3821
|
summary: "Show system-wide overview metrics",
|
|
2571
|
-
usage:
|
|
2572
|
-
run:
|
|
3822
|
+
usage: usage13,
|
|
3823
|
+
run: run13
|
|
2573
3824
|
};
|
|
2574
3825
|
|
|
2575
3826
|
// src/commands/studio.ts
|
|
2576
|
-
import { spawn } from "child_process";
|
|
2577
|
-
import { createReadStream, existsSync as
|
|
3827
|
+
import { spawn as spawn2 } from "child_process";
|
|
3828
|
+
import { createReadStream, existsSync as existsSync9, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
2578
3829
|
import { createServer } from "http";
|
|
2579
|
-
import { extname, join as
|
|
3830
|
+
import { extname, join as join9, normalize, resolve as resolve2, sep as sep3 } from "path";
|
|
2580
3831
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2581
|
-
import { parseArgs as
|
|
3832
|
+
import { parseArgs as parseArgs16 } from "util";
|
|
2582
3833
|
|
|
2583
3834
|
// src/commands/studio-admin.ts
|
|
2584
|
-
import { parseArgs as
|
|
3835
|
+
import { parseArgs as parseArgs15 } from "util";
|
|
2585
3836
|
import { password as passwordPrompt, text as text2 } from "@clack/prompts";
|
|
2586
3837
|
|
|
2587
3838
|
// ../../node_modules/.pnpm/postgres@3.4.9/node_modules/postgres/src/index.js
|
|
@@ -2595,9 +3846,9 @@ var originError = /* @__PURE__ */ Symbol("OriginError");
|
|
|
2595
3846
|
var CLOSE = {};
|
|
2596
3847
|
var Query = class extends Promise {
|
|
2597
3848
|
constructor(strings, args, handler, canceller, options = {}) {
|
|
2598
|
-
let
|
|
3849
|
+
let resolve3, reject;
|
|
2599
3850
|
super((a, b2) => {
|
|
2600
|
-
|
|
3851
|
+
resolve3 = a;
|
|
2601
3852
|
reject = b2;
|
|
2602
3853
|
});
|
|
2603
3854
|
this.tagged = Array.isArray(strings.raw);
|
|
@@ -2608,7 +3859,7 @@ var Query = class extends Promise {
|
|
|
2608
3859
|
this.options = options;
|
|
2609
3860
|
this.state = null;
|
|
2610
3861
|
this.statement = null;
|
|
2611
|
-
this.resolve = (x) => (this.active = false,
|
|
3862
|
+
this.resolve = (x) => (this.active = false, resolve3(x));
|
|
2612
3863
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2613
3864
|
this.active = false;
|
|
2614
3865
|
this.cancelled = null;
|
|
@@ -2656,12 +3907,12 @@ var Query = class extends Promise {
|
|
|
2656
3907
|
if (this.executed && !this.active)
|
|
2657
3908
|
return { done: true };
|
|
2658
3909
|
prev && prev();
|
|
2659
|
-
const promise = new Promise((
|
|
3910
|
+
const promise = new Promise((resolve3, reject) => {
|
|
2660
3911
|
this.cursorFn = (value) => {
|
|
2661
|
-
|
|
3912
|
+
resolve3({ value, done: false });
|
|
2662
3913
|
return new Promise((r) => prev = r);
|
|
2663
3914
|
};
|
|
2664
|
-
this.resolve = () => (this.active = false,
|
|
3915
|
+
this.resolve = () => (this.active = false, resolve3({ done: true }));
|
|
2665
3916
|
this.reject = (x) => (this.active = false, reject(x));
|
|
2666
3917
|
});
|
|
2667
3918
|
this.execute();
|
|
@@ -3288,12 +4539,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
3288
4539
|
x.on("drain", drain);
|
|
3289
4540
|
return x;
|
|
3290
4541
|
}
|
|
3291
|
-
async function cancel3({ pid, secret },
|
|
4542
|
+
async function cancel3({ pid, secret }, resolve3, reject) {
|
|
3292
4543
|
try {
|
|
3293
4544
|
cancelMessage = bytes_default().i32(16).i32(80877102).i32(pid).i32(secret).end(16);
|
|
3294
|
-
await
|
|
4545
|
+
await connect2();
|
|
3295
4546
|
socket.once("error", reject);
|
|
3296
|
-
socket.once("close",
|
|
4547
|
+
socket.once("close", resolve3);
|
|
3297
4548
|
} catch (error2) {
|
|
3298
4549
|
reject(error2);
|
|
3299
4550
|
}
|
|
@@ -3424,7 +4675,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
3424
4675
|
incomings = null;
|
|
3425
4676
|
}
|
|
3426
4677
|
}
|
|
3427
|
-
async function
|
|
4678
|
+
async function connect2() {
|
|
3428
4679
|
terminated = false;
|
|
3429
4680
|
backendParameters = {};
|
|
3430
4681
|
socket || (socket = await createSocket());
|
|
@@ -3443,7 +4694,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
|
|
3443
4694
|
hostIndex = (hostIndex + 1) % port.length;
|
|
3444
4695
|
}
|
|
3445
4696
|
function reconnect() {
|
|
3446
|
-
setTimeout(
|
|
4697
|
+
setTimeout(connect2, closedTime ? Math.max(0, closedTime + delay - performance.now()) : 0);
|
|
3447
4698
|
}
|
|
3448
4699
|
function connected() {
|
|
3449
4700
|
try {
|
|
@@ -4240,7 +5491,7 @@ function parseEvent(x) {
|
|
|
4240
5491
|
// ../../node_modules/.pnpm/postgres@3.4.9/node_modules/postgres/src/large.js
|
|
4241
5492
|
import Stream2 from "stream";
|
|
4242
5493
|
function largeObject(sql2, oid, mode = 131072 | 262144) {
|
|
4243
|
-
return new Promise(async (
|
|
5494
|
+
return new Promise(async (resolve3, reject) => {
|
|
4244
5495
|
await sql2.begin(async (sql3) => {
|
|
4245
5496
|
let finish;
|
|
4246
5497
|
!oid && ([{ oid }] = await sql3`select lo_creat(-1) as oid`);
|
|
@@ -4266,7 +5517,7 @@ function largeObject(sql2, oid, mode = 131072 | 262144) {
|
|
|
4266
5517
|
) seek
|
|
4267
5518
|
`
|
|
4268
5519
|
};
|
|
4269
|
-
|
|
5520
|
+
resolve3(lo);
|
|
4270
5521
|
return new Promise(async (r) => finish = r);
|
|
4271
5522
|
async function readable({
|
|
4272
5523
|
highWaterMark = 2048 * 8,
|
|
@@ -4440,10 +5691,10 @@ function Postgres(a, b2) {
|
|
|
4440
5691
|
}
|
|
4441
5692
|
async function reserve() {
|
|
4442
5693
|
const queue = queue_default();
|
|
4443
|
-
const c = open.length ? open.shift() : await new Promise((
|
|
4444
|
-
const query = { reserve:
|
|
5694
|
+
const c = open.length ? open.shift() : await new Promise((resolve3, reject) => {
|
|
5695
|
+
const query = { reserve: resolve3, reject };
|
|
4445
5696
|
queries.push(query);
|
|
4446
|
-
closed.length &&
|
|
5697
|
+
closed.length && connect2(closed.shift(), query);
|
|
4447
5698
|
});
|
|
4448
5699
|
move(c, reserved);
|
|
4449
5700
|
c.reserved = () => queue.length ? c.execute(queue.shift()) : move(c, reserved);
|
|
@@ -4478,9 +5729,9 @@ function Postgres(a, b2) {
|
|
|
4478
5729
|
let uncaughtError, result;
|
|
4479
5730
|
name && await sql3`savepoint ${sql3(name)}`;
|
|
4480
5731
|
try {
|
|
4481
|
-
result = await new Promise((
|
|
5732
|
+
result = await new Promise((resolve3, reject) => {
|
|
4482
5733
|
const x = fn2(sql3);
|
|
4483
|
-
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(
|
|
5734
|
+
Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve3, reject);
|
|
4484
5735
|
});
|
|
4485
5736
|
if (uncaughtError)
|
|
4486
5737
|
throw uncaughtError;
|
|
@@ -4530,15 +5781,15 @@ function Postgres(a, b2) {
|
|
|
4530
5781
|
if (open.length)
|
|
4531
5782
|
return go(open.shift(), query);
|
|
4532
5783
|
if (closed.length)
|
|
4533
|
-
return
|
|
5784
|
+
return connect2(closed.shift(), query);
|
|
4534
5785
|
busy.length ? go(busy.shift(), query) : queries.push(query);
|
|
4535
5786
|
}
|
|
4536
5787
|
function go(c, query) {
|
|
4537
5788
|
return c.execute(query) ? move(c, busy) : move(c, full);
|
|
4538
5789
|
}
|
|
4539
5790
|
function cancel3(query) {
|
|
4540
|
-
return new Promise((
|
|
4541
|
-
query.state ? query.active ? connection_default(options).cancel(query.state,
|
|
5791
|
+
return new Promise((resolve3, reject) => {
|
|
5792
|
+
query.state ? query.active ? connection_default(options).cancel(query.state, resolve3, reject) : query.cancelled = { resolve: resolve3, reject } : (queries.remove(query), query.cancelled = true, query.reject(Errors.generic("57014", "canceling statement due to user request")), resolve3());
|
|
4542
5793
|
});
|
|
4543
5794
|
}
|
|
4544
5795
|
async function end({ timeout = null } = {}) {
|
|
@@ -4557,13 +5808,13 @@ function Postgres(a, b2) {
|
|
|
4557
5808
|
async function close() {
|
|
4558
5809
|
await Promise.all(connections.map((c) => c.end()));
|
|
4559
5810
|
}
|
|
4560
|
-
async function destroy(
|
|
5811
|
+
async function destroy(resolve3) {
|
|
4561
5812
|
await Promise.all(connections.map((c) => c.terminate()));
|
|
4562
5813
|
while (queries.length)
|
|
4563
5814
|
queries.shift().reject(Errors.connection("CONNECTION_DESTROYED", options));
|
|
4564
|
-
|
|
5815
|
+
resolve3();
|
|
4565
5816
|
}
|
|
4566
|
-
function
|
|
5817
|
+
function connect2(c, query) {
|
|
4567
5818
|
move(c, connecting);
|
|
4568
5819
|
c.connect(query);
|
|
4569
5820
|
return c;
|
|
@@ -4588,7 +5839,7 @@ function Postgres(a, b2) {
|
|
|
4588
5839
|
c.reserved = null;
|
|
4589
5840
|
c.onclose && (c.onclose(e), c.onclose = null);
|
|
4590
5841
|
options.onclose && options.onclose(c.id);
|
|
4591
|
-
queries.length &&
|
|
5842
|
+
queries.length && connect2(c, queries.shift());
|
|
4592
5843
|
}
|
|
4593
5844
|
}
|
|
4594
5845
|
function parseOptions(a, b2) {
|
|
@@ -5818,7 +7069,7 @@ function sql(strings, ...params) {
|
|
|
5818
7069
|
return new SQL([new StringChunk(str)]);
|
|
5819
7070
|
}
|
|
5820
7071
|
sql2.raw = raw;
|
|
5821
|
-
function
|
|
7072
|
+
function join11(chunks, separator) {
|
|
5822
7073
|
const result = [];
|
|
5823
7074
|
for (const [i, chunk] of chunks.entries()) {
|
|
5824
7075
|
if (i > 0 && separator !== void 0) {
|
|
@@ -5828,7 +7079,7 @@ function sql(strings, ...params) {
|
|
|
5828
7079
|
}
|
|
5829
7080
|
return new SQL(result);
|
|
5830
7081
|
}
|
|
5831
|
-
sql2.join =
|
|
7082
|
+
sql2.join = join11;
|
|
5832
7083
|
function identifier(value) {
|
|
5833
7084
|
return new Name(value);
|
|
5834
7085
|
}
|
|
@@ -9478,7 +10729,7 @@ var PgSelectQueryBuilderBase = class extends TypedQueryBuilder {
|
|
|
9478
10729
|
const baseTableName = this.tableName;
|
|
9479
10730
|
const tableName = getTableLikeName(table);
|
|
9480
10731
|
for (const item of extractUsedTable(table)) this.usedTables.add(item);
|
|
9481
|
-
if (typeof tableName === "string" && this.config.joins?.some((
|
|
10732
|
+
if (typeof tableName === "string" && this.config.joins?.some((join11) => join11.alias === tableName)) {
|
|
9482
10733
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
9483
10734
|
}
|
|
9484
10735
|
if (!this.isPartialSelect) {
|
|
@@ -10699,7 +11950,7 @@ var PgUpdateBase = class extends QueryPromise {
|
|
|
10699
11950
|
createJoin(joinType) {
|
|
10700
11951
|
return (table, on) => {
|
|
10701
11952
|
const tableName = getTableLikeName(table);
|
|
10702
|
-
if (typeof tableName === "string" && this.config.joins.some((
|
|
11953
|
+
if (typeof tableName === "string" && this.config.joins.some((join11) => join11.alias === tableName)) {
|
|
10703
11954
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
10704
11955
|
}
|
|
10705
11956
|
if (typeof on === "function") {
|
|
@@ -10795,10 +12046,10 @@ var PgUpdateBase = class extends QueryPromise {
|
|
|
10795
12046
|
const fromFields = this.getTableLikeFields(this.config.from);
|
|
10796
12047
|
fields[tableName] = fromFields;
|
|
10797
12048
|
}
|
|
10798
|
-
for (const
|
|
10799
|
-
const tableName2 = getTableLikeName(
|
|
10800
|
-
if (typeof tableName2 === "string" && !is(
|
|
10801
|
-
const fromFields = this.getTableLikeFields(
|
|
12049
|
+
for (const join11 of this.config.joins) {
|
|
12050
|
+
const tableName2 = getTableLikeName(join11.table);
|
|
12051
|
+
if (typeof tableName2 === "string" && !is(join11.table, SQL)) {
|
|
12052
|
+
const fromFields = this.getTableLikeFields(join11.table);
|
|
10802
12053
|
fields[tableName2] = fromFields;
|
|
10803
12054
|
}
|
|
10804
12055
|
}
|
|
@@ -12350,6 +13601,12 @@ var emailSends = pgTable(
|
|
|
12350
13601
|
// mirrors the user_events idempotency pattern. Nullable: journey/system sends
|
|
12351
13602
|
// don't set it.
|
|
12352
13603
|
idempotencyKey: text("idempotency_key"),
|
|
13604
|
+
// Free-form per-send annotations. Set ONLY by test-mode redirected sends
|
|
13605
|
+
// today — `{ testMode: true, originalTo: <real recipient> }` — so Studio can
|
|
13606
|
+
// flag a TEST row and show who the mail was REALLY for. Nullable: normal
|
|
13607
|
+
// (live) sends leave it unset. jsonb (mirrors alert-history.payload) leaves
|
|
13608
|
+
// room for future markers without another migration.
|
|
13609
|
+
metadata: jsonb("metadata").$type(),
|
|
12353
13610
|
...timestamps
|
|
12354
13611
|
},
|
|
12355
13612
|
(table) => [
|
|
@@ -12946,7 +14203,7 @@ Examples:
|
|
|
12946
14203
|
Security: passwords are written ONLY via better-auth (scrypt) \u2014 never raw SQL,
|
|
12947
14204
|
never plaintext at rest, never logged. Prefer the masked prompt over --password.`;
|
|
12948
14205
|
function parseAdminFlags(argv) {
|
|
12949
|
-
const { values: values2 } =
|
|
14206
|
+
const { values: values2 } = parseArgs15({
|
|
12950
14207
|
args: argv,
|
|
12951
14208
|
allowPositionals: true,
|
|
12952
14209
|
strict: false,
|
|
@@ -13147,7 +14404,7 @@ async function runStudioAdmin(ctx, argv) {
|
|
|
13147
14404
|
}
|
|
13148
14405
|
|
|
13149
14406
|
// src/commands/studio.ts
|
|
13150
|
-
var
|
|
14407
|
+
var usage14 = `hogsend studio [options]
|
|
13151
14408
|
|
|
13152
14409
|
Subcommands:
|
|
13153
14410
|
admin create | reset | list Shell-gated Studio admin recovery (DB + secret).
|
|
@@ -13179,16 +14436,16 @@ Examples:
|
|
|
13179
14436
|
function resolveStudioDist(distFlag) {
|
|
13180
14437
|
const candidates = [];
|
|
13181
14438
|
if (distFlag && distFlag.length > 0) {
|
|
13182
|
-
candidates.push(
|
|
14439
|
+
candidates.push(resolve2(process.cwd(), distFlag));
|
|
13183
14440
|
}
|
|
13184
14441
|
candidates.push(fileURLToPath2(new URL("../studio", import.meta.url)));
|
|
13185
14442
|
candidates.push(
|
|
13186
14443
|
fileURLToPath2(new URL("../../studio/dist", import.meta.url)),
|
|
13187
14444
|
fileURLToPath2(new URL("../../../studio/dist", import.meta.url))
|
|
13188
14445
|
);
|
|
13189
|
-
candidates.push(
|
|
14446
|
+
candidates.push(resolve2(process.cwd(), "packages/studio/dist"));
|
|
13190
14447
|
for (const dir of candidates) {
|
|
13191
|
-
if (
|
|
14448
|
+
if (existsSync9(join9(dir, "index.html"))) {
|
|
13192
14449
|
return dir;
|
|
13193
14450
|
}
|
|
13194
14451
|
}
|
|
@@ -13215,7 +14472,7 @@ function mimeFor(path) {
|
|
|
13215
14472
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
13216
14473
|
}
|
|
13217
14474
|
function indexHtml(distPath, baseUrl) {
|
|
13218
|
-
const raw =
|
|
14475
|
+
const raw = readFileSync5(join9(distPath, "index.html"), "utf8");
|
|
13219
14476
|
if (!baseUrl) return raw;
|
|
13220
14477
|
const inject = `<script>window.__HOGSEND_STUDIO__=${JSON.stringify({
|
|
13221
14478
|
baseUrl
|
|
@@ -13230,19 +14487,19 @@ function openBrowser(url) {
|
|
|
13230
14487
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
13231
14488
|
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
13232
14489
|
try {
|
|
13233
|
-
const child =
|
|
14490
|
+
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
13234
14491
|
child.on("error", () => {
|
|
13235
14492
|
});
|
|
13236
14493
|
child.unref();
|
|
13237
14494
|
} catch {
|
|
13238
14495
|
}
|
|
13239
14496
|
}
|
|
13240
|
-
async function
|
|
14497
|
+
async function run14(ctx) {
|
|
13241
14498
|
if (ctx.argv[0] === "admin") {
|
|
13242
14499
|
await runStudioAdmin(ctx, ctx.argv.slice(1));
|
|
13243
14500
|
return;
|
|
13244
14501
|
}
|
|
13245
|
-
const { values: values2, positionals } =
|
|
14502
|
+
const { values: values2, positionals } = parseArgs16({
|
|
13246
14503
|
args: ctx.argv,
|
|
13247
14504
|
allowPositionals: true,
|
|
13248
14505
|
strict: false,
|
|
@@ -13255,7 +14512,7 @@ async function run12(ctx) {
|
|
|
13255
14512
|
}
|
|
13256
14513
|
});
|
|
13257
14514
|
if (values2.help) {
|
|
13258
|
-
ctx.out.log(
|
|
14515
|
+
ctx.out.log(usage14);
|
|
13259
14516
|
return;
|
|
13260
14517
|
}
|
|
13261
14518
|
const port = Number(values2.port ?? "3333");
|
|
@@ -13281,13 +14538,13 @@ async function run12(ctx) {
|
|
|
13281
14538
|
res.end(index2);
|
|
13282
14539
|
return;
|
|
13283
14540
|
}
|
|
13284
|
-
const target = normalize(
|
|
14541
|
+
const target = normalize(join9(distPath, rel));
|
|
13285
14542
|
if (target !== distPath && !target.startsWith(distPath + sep3)) {
|
|
13286
14543
|
res.writeHead(403);
|
|
13287
14544
|
res.end("Forbidden");
|
|
13288
14545
|
return;
|
|
13289
14546
|
}
|
|
13290
|
-
if (
|
|
14547
|
+
if (existsSync9(target) && statSync2(target).isFile()) {
|
|
13291
14548
|
res.writeHead(200, { "content-type": mimeFor(target) });
|
|
13292
14549
|
createReadStream(target).pipe(res);
|
|
13293
14550
|
return;
|
|
@@ -13341,17 +14598,17 @@ async function run12(ctx) {
|
|
|
13341
14598
|
var studioCommand = {
|
|
13342
14599
|
name: "studio",
|
|
13343
14600
|
summary: "Serve the bundled Hogsend Studio admin SPA locally",
|
|
13344
|
-
usage:
|
|
13345
|
-
run:
|
|
14601
|
+
usage: usage14,
|
|
14602
|
+
run: run14
|
|
13346
14603
|
};
|
|
13347
14604
|
|
|
13348
14605
|
// src/commands/upgrade.ts
|
|
13349
|
-
import { spawnSync as
|
|
13350
|
-
import { existsSync as
|
|
13351
|
-
import { join as
|
|
13352
|
-
import { parseArgs as
|
|
13353
|
-
import { confirm as
|
|
13354
|
-
var
|
|
14606
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
14607
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
14608
|
+
import { join as join10 } from "path";
|
|
14609
|
+
import { parseArgs as parseArgs17 } from "util";
|
|
14610
|
+
import { confirm as confirm3 } from "@clack/prompts";
|
|
14611
|
+
var usage15 = `hogsend upgrade [--cwd <dir>] [--pm <pnpm|npm|yarn|bun>] [options]
|
|
13355
14612
|
|
|
13356
14613
|
Upgrade a scaffolded Hogsend app in one step:
|
|
13357
14614
|
1. bump every @hogsend/* dependency to latest (or --to <version>), then
|
|
@@ -13372,23 +14629,23 @@ Options:
|
|
|
13372
14629
|
-h, --help Show this help.`;
|
|
13373
14630
|
var VALID_PMS = ["pnpm", "npm", "yarn", "bun"];
|
|
13374
14631
|
function detectPm(cwd) {
|
|
13375
|
-
if (
|
|
13376
|
-
if (
|
|
13377
|
-
if (
|
|
14632
|
+
if (existsSync10(join10(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
14633
|
+
if (existsSync10(join10(cwd, "yarn.lock"))) return "yarn";
|
|
14634
|
+
if (existsSync10(join10(cwd, "bun.lockb")) || existsSync10(join10(cwd, "bun.lock")))
|
|
13378
14635
|
return "bun";
|
|
13379
|
-
if (
|
|
14636
|
+
if (existsSync10(join10(cwd, "package-lock.json"))) return "npm";
|
|
13380
14637
|
return "pnpm";
|
|
13381
14638
|
}
|
|
13382
14639
|
function hogsendDeps(cwd) {
|
|
13383
|
-
const pkg = JSON.parse(
|
|
14640
|
+
const pkg = JSON.parse(readFileSync6(join10(cwd, "package.json"), "utf8"));
|
|
13384
14641
|
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
13385
14642
|
return Object.keys(all).filter((n) => n.startsWith("@hogsend/")).sort();
|
|
13386
14643
|
}
|
|
13387
14644
|
function addArgs(pm, specs) {
|
|
13388
14645
|
return [pm === "npm" ? "install" : "add", ...specs];
|
|
13389
14646
|
}
|
|
13390
|
-
async function
|
|
13391
|
-
const { values: values2 } =
|
|
14647
|
+
async function run15(ctx) {
|
|
14648
|
+
const { values: values2 } = parseArgs17({
|
|
13392
14649
|
args: ctx.argv,
|
|
13393
14650
|
allowPositionals: true,
|
|
13394
14651
|
options: {
|
|
@@ -13402,14 +14659,14 @@ async function run13(ctx) {
|
|
|
13402
14659
|
}
|
|
13403
14660
|
});
|
|
13404
14661
|
if (values2.help) {
|
|
13405
|
-
ctx.out.log(
|
|
14662
|
+
ctx.out.log(usage15);
|
|
13406
14663
|
return;
|
|
13407
14664
|
}
|
|
13408
14665
|
if (values2["deps-only"] && values2["skills-only"]) {
|
|
13409
14666
|
ctx.out.fail("--deps-only and --skills-only are mutually exclusive.");
|
|
13410
14667
|
}
|
|
13411
14668
|
const cwd = values2.cwd ?? process.cwd();
|
|
13412
|
-
if (!
|
|
14669
|
+
if (!existsSync10(join10(cwd, "package.json"))) {
|
|
13413
14670
|
ctx.out.fail(
|
|
13414
14671
|
`no package.json in ${cwd} \u2014 run upgrade from a scaffolded Hogsend app (or pass --cwd).`
|
|
13415
14672
|
);
|
|
@@ -13431,7 +14688,7 @@ async function run13(ctx) {
|
|
|
13431
14688
|
const deps = doDeps ? hogsendDeps(cwd) : [];
|
|
13432
14689
|
if (doDeps && deps.length === 0) {
|
|
13433
14690
|
ctx.out.fail(
|
|
13434
|
-
`no @hogsend/* dependencies found in ${
|
|
14691
|
+
`no @hogsend/* dependencies found in ${join10(cwd, "package.json")}.`
|
|
13435
14692
|
);
|
|
13436
14693
|
}
|
|
13437
14694
|
const skipConfirm = ctx.json || values2.yes;
|
|
@@ -13446,7 +14703,7 @@ async function run13(ctx) {
|
|
|
13446
14703
|
doSkills ? "refresh .claude/skills" : null
|
|
13447
14704
|
].filter(Boolean).join(" + ");
|
|
13448
14705
|
const proceed = bail(
|
|
13449
|
-
await
|
|
14706
|
+
await confirm3({ message: `Upgrade ${color.cyan(cwd)}: ${plan}?` })
|
|
13450
14707
|
);
|
|
13451
14708
|
if (!proceed) {
|
|
13452
14709
|
ctx.out.outro(color.dim("Nothing changed."));
|
|
@@ -13458,7 +14715,7 @@ async function run13(ctx) {
|
|
|
13458
14715
|
const specs = deps.map((n) => `${n}@${target}`);
|
|
13459
14716
|
const dep = await ctx.out.step(
|
|
13460
14717
|
`Bumping @hogsend/* -> ${target} (${pm})`,
|
|
13461
|
-
async () =>
|
|
14718
|
+
async () => spawnSync4(pm, addArgs(pm, specs), {
|
|
13462
14719
|
cwd,
|
|
13463
14720
|
stdio: ctx.json ? "ignore" : "inherit",
|
|
13464
14721
|
shell: process.platform === "win32"
|
|
@@ -13527,12 +14784,12 @@ async function run13(ctx) {
|
|
|
13527
14784
|
var upgradeCommand = {
|
|
13528
14785
|
name: "upgrade",
|
|
13529
14786
|
summary: "Bump @hogsend/* deps to latest + refresh vendored skills",
|
|
13530
|
-
usage:
|
|
13531
|
-
run:
|
|
14787
|
+
usage: usage15,
|
|
14788
|
+
run: run15
|
|
13532
14789
|
};
|
|
13533
14790
|
|
|
13534
14791
|
// src/commands/webhooks.ts
|
|
13535
|
-
import { parseArgs as
|
|
14792
|
+
import { parseArgs as parseArgs18 } from "util";
|
|
13536
14793
|
var WEBHOOK_EVENT_TYPES = [
|
|
13537
14794
|
"contact.created",
|
|
13538
14795
|
"contact.updated",
|
|
@@ -13548,7 +14805,7 @@ var WEBHOOK_EVENT_TYPES = [
|
|
|
13548
14805
|
"bucket.entered",
|
|
13549
14806
|
"bucket.left"
|
|
13550
14807
|
];
|
|
13551
|
-
var
|
|
14808
|
+
var usage16 = `hogsend webhooks <subcommand> [options]
|
|
13552
14809
|
|
|
13553
14810
|
Manage outbound webhook endpoints \u2014 the Svix-style signed event stream Hogsend
|
|
13554
14811
|
emits to your URLs. Wraps the admin routes (/v1/admin/webhooks), so this command
|
|
@@ -13594,7 +14851,7 @@ Examples:
|
|
|
13594
14851
|
hogsend webhooks list --include-disabled
|
|
13595
14852
|
hogsend webhooks rotate-secret we_123
|
|
13596
14853
|
hogsend webhooks test we_123`;
|
|
13597
|
-
var
|
|
14854
|
+
var badge6 = `${color.bgMagenta(color.black(" hogsend "))} webhooks`;
|
|
13598
14855
|
async function fetchOrFail2(ctx, label, fn) {
|
|
13599
14856
|
try {
|
|
13600
14857
|
return await ctx.out.step(label, fn);
|
|
@@ -13638,7 +14895,7 @@ ${color.bold(secret)}`,
|
|
|
13638
14895
|
);
|
|
13639
14896
|
}
|
|
13640
14897
|
async function runList5(ctx, argv) {
|
|
13641
|
-
const { values: values2 } =
|
|
14898
|
+
const { values: values2 } = parseArgs18({
|
|
13642
14899
|
args: argv,
|
|
13643
14900
|
allowPositionals: true,
|
|
13644
14901
|
options: {
|
|
@@ -13649,7 +14906,7 @@ async function runList5(ctx, argv) {
|
|
|
13649
14906
|
}
|
|
13650
14907
|
});
|
|
13651
14908
|
if (values2.help) {
|
|
13652
|
-
ctx.out.log(
|
|
14909
|
+
ctx.out.log(usage16);
|
|
13653
14910
|
return;
|
|
13654
14911
|
}
|
|
13655
14912
|
const query = {
|
|
@@ -13657,7 +14914,7 @@ async function runList5(ctx, argv) {
|
|
|
13657
14914
|
limit: values2.limit,
|
|
13658
14915
|
offset: values2.offset
|
|
13659
14916
|
};
|
|
13660
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
14917
|
+
if (!ctx.json) ctx.out.intro(`${badge6} list`);
|
|
13661
14918
|
const res = await fetchOrFail2(
|
|
13662
14919
|
ctx,
|
|
13663
14920
|
"Fetching webhooks",
|
|
@@ -13698,13 +14955,13 @@ function renderEndpoint(ctx, ep, title) {
|
|
|
13698
14955
|
);
|
|
13699
14956
|
}
|
|
13700
14957
|
async function runGet3(ctx, argv) {
|
|
13701
|
-
const { values: values2, positionals } =
|
|
14958
|
+
const { values: values2, positionals } = parseArgs18({
|
|
13702
14959
|
args: argv,
|
|
13703
14960
|
allowPositionals: true,
|
|
13704
14961
|
options: { help: { type: "boolean", short: "h", default: false } }
|
|
13705
14962
|
});
|
|
13706
14963
|
if (values2.help) {
|
|
13707
|
-
ctx.out.log(
|
|
14964
|
+
ctx.out.log(usage16);
|
|
13708
14965
|
return;
|
|
13709
14966
|
}
|
|
13710
14967
|
const id = positionals[0];
|
|
@@ -13713,7 +14970,7 @@ async function runGet3(ctx, argv) {
|
|
|
13713
14970
|
"webhooks get requires an endpoint id, e.g. hogsend webhooks get we_123"
|
|
13714
14971
|
);
|
|
13715
14972
|
}
|
|
13716
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
14973
|
+
if (!ctx.json) ctx.out.intro(`${badge6} get`);
|
|
13717
14974
|
const res = await fetchOrFail2(
|
|
13718
14975
|
ctx,
|
|
13719
14976
|
"Fetching webhook",
|
|
@@ -13729,7 +14986,7 @@ async function runGet3(ctx, argv) {
|
|
|
13729
14986
|
ctx.out.outro(`${res.url} \u2192 ${res.status}`);
|
|
13730
14987
|
}
|
|
13731
14988
|
async function runCreate2(ctx, argv) {
|
|
13732
|
-
const { values: values2 } =
|
|
14989
|
+
const { values: values2 } = parseArgs18({
|
|
13733
14990
|
args: argv,
|
|
13734
14991
|
allowPositionals: true,
|
|
13735
14992
|
options: {
|
|
@@ -13742,7 +14999,7 @@ async function runCreate2(ctx, argv) {
|
|
|
13742
14999
|
}
|
|
13743
15000
|
});
|
|
13744
15001
|
if (values2.help) {
|
|
13745
|
-
ctx.out.log(
|
|
15002
|
+
ctx.out.log(usage16);
|
|
13746
15003
|
return;
|
|
13747
15004
|
}
|
|
13748
15005
|
const url = values2.url;
|
|
@@ -13760,7 +15017,7 @@ async function runCreate2(ctx, argv) {
|
|
|
13760
15017
|
const body = { url, eventTypes };
|
|
13761
15018
|
if (values2.description !== void 0) body.description = values2.description;
|
|
13762
15019
|
if (values2.disabled) body.disabled = true;
|
|
13763
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
15020
|
+
if (!ctx.json) ctx.out.intro(`${badge6} create`);
|
|
13764
15021
|
const res = await fetchOrFail2(
|
|
13765
15022
|
ctx,
|
|
13766
15023
|
"Creating webhook",
|
|
@@ -13776,7 +15033,7 @@ async function runCreate2(ctx, argv) {
|
|
|
13776
15033
|
ctx.out.outro(`${color.green("Created")} ${res.id} \u2192 ${res.url}`);
|
|
13777
15034
|
}
|
|
13778
15035
|
async function runUpdate(ctx, argv) {
|
|
13779
|
-
const { values: values2, positionals } =
|
|
15036
|
+
const { values: values2, positionals } = parseArgs18({
|
|
13780
15037
|
args: argv,
|
|
13781
15038
|
allowPositionals: true,
|
|
13782
15039
|
options: {
|
|
@@ -13790,7 +15047,7 @@ async function runUpdate(ctx, argv) {
|
|
|
13790
15047
|
}
|
|
13791
15048
|
});
|
|
13792
15049
|
if (values2.help) {
|
|
13793
|
-
ctx.out.log(
|
|
15050
|
+
ctx.out.log(usage16);
|
|
13794
15051
|
return;
|
|
13795
15052
|
}
|
|
13796
15053
|
const id = positionals[0];
|
|
@@ -13814,7 +15071,7 @@ async function runUpdate(ctx, argv) {
|
|
|
13814
15071
|
"webhooks update: nothing to change \u2014 pass --url / --event / --description / --disabled / --enabled"
|
|
13815
15072
|
);
|
|
13816
15073
|
}
|
|
13817
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
15074
|
+
if (!ctx.json) ctx.out.intro(`${badge6} update`);
|
|
13818
15075
|
const res = await fetchOrFail2(
|
|
13819
15076
|
ctx,
|
|
13820
15077
|
"Updating webhook",
|
|
@@ -13831,13 +15088,13 @@ async function runUpdate(ctx, argv) {
|
|
|
13831
15088
|
ctx.out.outro(`${color.green("Updated")} ${res.id} \u2192 ${res.status}`);
|
|
13832
15089
|
}
|
|
13833
15090
|
async function runDelete(ctx, argv) {
|
|
13834
|
-
const { values: values2, positionals } =
|
|
15091
|
+
const { values: values2, positionals } = parseArgs18({
|
|
13835
15092
|
args: argv,
|
|
13836
15093
|
allowPositionals: true,
|
|
13837
15094
|
options: { help: { type: "boolean", short: "h", default: false } }
|
|
13838
15095
|
});
|
|
13839
15096
|
if (values2.help) {
|
|
13840
|
-
ctx.out.log(
|
|
15097
|
+
ctx.out.log(usage16);
|
|
13841
15098
|
return;
|
|
13842
15099
|
}
|
|
13843
15100
|
const id = positionals[0];
|
|
@@ -13846,7 +15103,7 @@ async function runDelete(ctx, argv) {
|
|
|
13846
15103
|
"webhooks delete requires an endpoint id, e.g. hogsend webhooks delete we_123"
|
|
13847
15104
|
);
|
|
13848
15105
|
}
|
|
13849
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
15106
|
+
if (!ctx.json) ctx.out.intro(`${badge6} delete`);
|
|
13850
15107
|
const res = await fetchOrFail2(
|
|
13851
15108
|
ctx,
|
|
13852
15109
|
"Deleting webhook",
|
|
@@ -13861,13 +15118,13 @@ async function runDelete(ctx, argv) {
|
|
|
13861
15118
|
ctx.out.outro(`${color.green("Deleted")} ${id}`);
|
|
13862
15119
|
}
|
|
13863
15120
|
async function runRotate(ctx, argv) {
|
|
13864
|
-
const { values: values2, positionals } =
|
|
15121
|
+
const { values: values2, positionals } = parseArgs18({
|
|
13865
15122
|
args: argv,
|
|
13866
15123
|
allowPositionals: true,
|
|
13867
15124
|
options: { help: { type: "boolean", short: "h", default: false } }
|
|
13868
15125
|
});
|
|
13869
15126
|
if (values2.help) {
|
|
13870
|
-
ctx.out.log(
|
|
15127
|
+
ctx.out.log(usage16);
|
|
13871
15128
|
return;
|
|
13872
15129
|
}
|
|
13873
15130
|
const id = positionals[0];
|
|
@@ -13876,7 +15133,7 @@ async function runRotate(ctx, argv) {
|
|
|
13876
15133
|
"webhooks rotate-secret requires an endpoint id, e.g. hogsend webhooks rotate-secret we_123"
|
|
13877
15134
|
);
|
|
13878
15135
|
}
|
|
13879
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
15136
|
+
if (!ctx.json) ctx.out.intro(`${badge6} rotate-secret`);
|
|
13880
15137
|
const res = await fetchOrFail2(
|
|
13881
15138
|
ctx,
|
|
13882
15139
|
"Rotating signing secret",
|
|
@@ -13896,13 +15153,13 @@ async function runRotate(ctx, argv) {
|
|
|
13896
15153
|
);
|
|
13897
15154
|
}
|
|
13898
15155
|
async function runTest(ctx, argv) {
|
|
13899
|
-
const { values: values2, positionals } =
|
|
15156
|
+
const { values: values2, positionals } = parseArgs18({
|
|
13900
15157
|
args: argv,
|
|
13901
15158
|
allowPositionals: true,
|
|
13902
15159
|
options: { help: { type: "boolean", short: "h", default: false } }
|
|
13903
15160
|
});
|
|
13904
15161
|
if (values2.help) {
|
|
13905
|
-
ctx.out.log(
|
|
15162
|
+
ctx.out.log(usage16);
|
|
13906
15163
|
return;
|
|
13907
15164
|
}
|
|
13908
15165
|
const id = positionals[0];
|
|
@@ -13911,7 +15168,7 @@ async function runTest(ctx, argv) {
|
|
|
13911
15168
|
"webhooks test requires an endpoint id, e.g. hogsend webhooks test we_123"
|
|
13912
15169
|
);
|
|
13913
15170
|
}
|
|
13914
|
-
if (!ctx.json) ctx.out.intro(`${
|
|
15171
|
+
if (!ctx.json) ctx.out.intro(`${badge6} test`);
|
|
13915
15172
|
const res = await fetchOrFail2(
|
|
13916
15173
|
ctx,
|
|
13917
15174
|
"Enqueuing test delivery",
|
|
@@ -13928,7 +15185,7 @@ async function runTest(ctx, argv) {
|
|
|
13928
15185
|
`${color.green("Enqueued")} a ${color.cyan(res.eventType)} delivery to ${id}.`
|
|
13929
15186
|
);
|
|
13930
15187
|
}
|
|
13931
|
-
async function
|
|
15188
|
+
async function run16(ctx) {
|
|
13932
15189
|
const sub = ctx.argv[0];
|
|
13933
15190
|
switch (sub) {
|
|
13934
15191
|
case "list":
|
|
@@ -13959,8 +15216,8 @@ async function run14(ctx) {
|
|
|
13959
15216
|
var webhooksCommand = {
|
|
13960
15217
|
name: "webhooks",
|
|
13961
15218
|
summary: "Manage outbound webhook endpoints (create, rotate, test)",
|
|
13962
|
-
usage:
|
|
13963
|
-
run:
|
|
15219
|
+
usage: usage16,
|
|
15220
|
+
run: run16
|
|
13964
15221
|
};
|
|
13965
15222
|
|
|
13966
15223
|
// src/commands/index.ts
|
|
@@ -13973,7 +15230,9 @@ var commands = [
|
|
|
13973
15230
|
emailsCommand,
|
|
13974
15231
|
campaignsCommand,
|
|
13975
15232
|
webhooksCommand,
|
|
15233
|
+
domainCommand,
|
|
13976
15234
|
studioCommand,
|
|
15235
|
+
devCommand,
|
|
13977
15236
|
setupCommand,
|
|
13978
15237
|
skillsCommand,
|
|
13979
15238
|
upgradeCommand,
|
|
@@ -13981,87 +15240,6 @@ var commands = [
|
|
|
13981
15240
|
patchCommand
|
|
13982
15241
|
];
|
|
13983
15242
|
|
|
13984
|
-
// src/lib/config.ts
|
|
13985
|
-
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
13986
|
-
import { join as join8 } from "path";
|
|
13987
|
-
import { parseArgs as parseArgs16 } from "util";
|
|
13988
|
-
var DEFAULT_BASE_URL = "http://localhost:3002";
|
|
13989
|
-
function parseGlobalFlags(argv) {
|
|
13990
|
-
const { values: values2, tokens } = parseArgs16({
|
|
13991
|
-
args: argv,
|
|
13992
|
-
allowPositionals: true,
|
|
13993
|
-
strict: false,
|
|
13994
|
-
tokens: true,
|
|
13995
|
-
options: {
|
|
13996
|
-
url: { type: "string" },
|
|
13997
|
-
"admin-key": { type: "string" },
|
|
13998
|
-
"data-key": { type: "string" },
|
|
13999
|
-
json: { type: "boolean", default: false },
|
|
14000
|
-
help: { type: "boolean", short: "h", default: false }
|
|
14001
|
-
}
|
|
14002
|
-
});
|
|
14003
|
-
const owned = /* @__PURE__ */ new Set(["url", "admin-key", "data-key", "json", "help", "h"]);
|
|
14004
|
-
const rest = [];
|
|
14005
|
-
for (const token of tokens) {
|
|
14006
|
-
if (token.kind === "positional") {
|
|
14007
|
-
rest.push(token.value);
|
|
14008
|
-
} else if (token.kind === "option") {
|
|
14009
|
-
if (owned.has(token.name)) continue;
|
|
14010
|
-
rest.push(token.rawName);
|
|
14011
|
-
if (token.value !== void 0 && !token.inlineValue) {
|
|
14012
|
-
rest.push(token.value);
|
|
14013
|
-
} else if (token.inlineValue && token.value !== void 0) {
|
|
14014
|
-
rest[rest.length - 1] = `${token.rawName}=${token.value}`;
|
|
14015
|
-
}
|
|
14016
|
-
}
|
|
14017
|
-
}
|
|
14018
|
-
return {
|
|
14019
|
-
url: typeof values2.url === "string" ? values2.url : void 0,
|
|
14020
|
-
adminKey: typeof values2["admin-key"] === "string" ? values2["admin-key"] : void 0,
|
|
14021
|
-
dataKey: typeof values2["data-key"] === "string" ? values2["data-key"] : void 0,
|
|
14022
|
-
json: values2.json === true,
|
|
14023
|
-
help: values2.help === true,
|
|
14024
|
-
rest
|
|
14025
|
-
};
|
|
14026
|
-
}
|
|
14027
|
-
function loadDotEnv(cwd = process.cwd()) {
|
|
14028
|
-
const out = {};
|
|
14029
|
-
const file = join8(cwd, ".env");
|
|
14030
|
-
if (!existsSync8(file)) return out;
|
|
14031
|
-
let raw;
|
|
14032
|
-
try {
|
|
14033
|
-
raw = readFileSync5(file, "utf8");
|
|
14034
|
-
} catch {
|
|
14035
|
-
return out;
|
|
14036
|
-
}
|
|
14037
|
-
for (const rawLine of raw.split(/\r?\n/)) {
|
|
14038
|
-
const line2 = rawLine.trim();
|
|
14039
|
-
if (line2 === "" || line2.startsWith("#")) continue;
|
|
14040
|
-
const withoutExport = line2.startsWith("export ") ? line2.slice("export ".length) : line2;
|
|
14041
|
-
const eq2 = withoutExport.indexOf("=");
|
|
14042
|
-
if (eq2 === -1) continue;
|
|
14043
|
-
const key = withoutExport.slice(0, eq2).trim();
|
|
14044
|
-
if (key === "") continue;
|
|
14045
|
-
let value = withoutExport.slice(eq2 + 1).trim();
|
|
14046
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
14047
|
-
value = value.slice(1, -1);
|
|
14048
|
-
}
|
|
14049
|
-
out[key] = value;
|
|
14050
|
-
}
|
|
14051
|
-
return out;
|
|
14052
|
-
}
|
|
14053
|
-
function resolveConfig(flags, cwd = process.cwd()) {
|
|
14054
|
-
const dotenv = loadDotEnv(cwd);
|
|
14055
|
-
const baseUrlRaw = flags.url ?? process.env.HOGSEND_API_URL ?? dotenv.HOGSEND_API_URL ?? DEFAULT_BASE_URL;
|
|
14056
|
-
const adminKey = flags.adminKey ?? process.env.HOGSEND_ADMIN_KEY ?? process.env.ADMIN_API_KEY ?? dotenv.HOGSEND_ADMIN_KEY ?? dotenv.ADMIN_API_KEY;
|
|
14057
|
-
const dataKey = flags.dataKey ?? process.env.HOGSEND_DATA_KEY ?? process.env.HOGSEND_API_KEY ?? dotenv.HOGSEND_DATA_KEY ?? dotenv.HOGSEND_API_KEY;
|
|
14058
|
-
return {
|
|
14059
|
-
baseUrl: baseUrlRaw.replace(/\/+$/, ""),
|
|
14060
|
-
adminKey: adminKey && adminKey.length > 0 ? adminKey : void 0,
|
|
14061
|
-
dataKey: dataKey && dataKey.length > 0 ? dataKey : void 0
|
|
14062
|
-
};
|
|
14063
|
-
}
|
|
14064
|
-
|
|
14065
15243
|
// src/bin.ts
|
|
14066
15244
|
function version2() {
|
|
14067
15245
|
try {
|