@hogsend/cli 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +382 -76
- package/dist/bin.js.map +1 -1
- package/package.json +4 -4
- package/skills/hogsend-database/references/migrations.md +1 -1
- package/skills/hogsend-deploy/SKILL.md +11 -0
- package/src/__tests__/hatchet-token.test.ts +205 -0
- package/src/commands/hatchet.ts +181 -0
- package/src/commands/index.ts +2 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/hatchet-token.ts +268 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hogsend/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"tsup": "^8.5.1",
|
|
33
33
|
"tsx": "^4.22.4",
|
|
34
34
|
"vitest": "^4.1.7",
|
|
35
|
-
"@hogsend/studio": "^0.
|
|
35
|
+
"@hogsend/studio": "^0.13.0",
|
|
36
36
|
"@repo/typescript-config": "0.0.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@clack/prompts": "^1.5.0",
|
|
43
43
|
"better-auth": "^1.6.11",
|
|
44
44
|
"picocolors": "^1.1.1",
|
|
45
|
-
"@hogsend/db": "^0.
|
|
46
|
-
"@hogsend/engine": "^0.
|
|
45
|
+
"@hogsend/db": "^0.13.0",
|
|
46
|
+
"@hogsend/engine": "^0.13.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"prebuild": "node scripts/bundle-studio.mjs",
|
|
@@ -33,6 +33,17 @@ This is the **consumer** deploy guide — for shipping the app you scaffolded wi
|
|
|
33
33
|
- **Hatchet-Lite is your orchestration engine.** The worker connects to it over
|
|
34
34
|
gRPC with `HATCHET_CLIENT_TOKEN` + `HATCHET_CLIENT_HOST_PORT`. Locally it runs
|
|
35
35
|
via `docker-compose.yml`; in prod it's its own service.
|
|
36
|
+
- **Mint `HATCHET_CLIENT_TOKEN` headlessly.** `hogsend hatchet token
|
|
37
|
+
--url <hatchet-url> --email <e> --password <p> [--tenant <slug>]` drives
|
|
38
|
+
hatchet-lite's REST API (register-or-login → ensure tenant → create token)
|
|
39
|
+
and prints ONLY the token to stdout — pipe it straight into
|
|
40
|
+
`railway variables --set "HATCHET_CLIENT_TOKEN=$(...)"`. No dashboard
|
|
41
|
+
copy-paste needed.
|
|
42
|
+
- **Lock down a public hatchet-lite.** It ships with OPEN registration — anyone
|
|
43
|
+
who finds the public URL can create an account on your Hatchet dashboard. Set
|
|
44
|
+
`SERVER_ALLOW_SIGNUP=false` and seed a real admin via `ADMIN_EMAIL` /
|
|
45
|
+
`ADMIN_PASSWORD` (never leave the `admin@example.com` / `Admin123!!`
|
|
46
|
+
defaults). `hogsend hatchet token` then logs in with those credentials.
|
|
36
47
|
- **Three required env vars, the rest optional.** `DATABASE_URL`,
|
|
37
48
|
`BETTER_AUTH_SECRET`, `RESEND_API_KEY` are hard-required at boot; PostHog,
|
|
38
49
|
webhook secrets, and the admin key are opt-in.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { HatchetTokenError, mintHatchetToken } from "../lib/hatchet-token.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unit tests for the headless HATCHET_CLIENT_TOKEN mint flow
|
|
6
|
+
* (register-or-login → ensure tenant → create API token) against a scripted
|
|
7
|
+
* fake of hatchet-lite's REST API. Covers:
|
|
8
|
+
* - happy path: fresh register → seeded "default" tenant found → token
|
|
9
|
+
* - locked-down instance: register 400 ("user signups are disabled") →
|
|
10
|
+
* login fallback with the seeded admin credentials
|
|
11
|
+
* - tenant missing from memberships → created with engineVersion V1
|
|
12
|
+
* - bad credentials → HatchetTokenError mentioning login
|
|
13
|
+
* - token is read from CreateAPITokenResponse.token verbatim
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface Call {
|
|
17
|
+
url: string;
|
|
18
|
+
method: string;
|
|
19
|
+
body?: unknown;
|
|
20
|
+
cookie?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Route = (call: Call) => Response;
|
|
24
|
+
|
|
25
|
+
function fakeHatchet(routes: Record<string, Route>): {
|
|
26
|
+
fetchImpl: typeof fetch;
|
|
27
|
+
calls: Call[];
|
|
28
|
+
} {
|
|
29
|
+
const calls: Call[] = [];
|
|
30
|
+
const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
31
|
+
const url = String(input);
|
|
32
|
+
const path = new URL(url).pathname;
|
|
33
|
+
const headers = new Headers(init?.headers);
|
|
34
|
+
const call: Call = {
|
|
35
|
+
url,
|
|
36
|
+
method: init?.method ?? "GET",
|
|
37
|
+
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
38
|
+
cookie: headers.get("cookie") ?? undefined,
|
|
39
|
+
};
|
|
40
|
+
calls.push(call);
|
|
41
|
+
const route = routes[`${call.method} ${path}`];
|
|
42
|
+
if (!route) {
|
|
43
|
+
return new Response(JSON.stringify({ errors: [] }), { status: 404 });
|
|
44
|
+
}
|
|
45
|
+
return route(call);
|
|
46
|
+
}) as typeof fetch;
|
|
47
|
+
return { fetchImpl, calls };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const json = (body: unknown, init?: ResponseInit) =>
|
|
51
|
+
new Response(JSON.stringify(body), {
|
|
52
|
+
status: 200,
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
...init,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const loginOk = () =>
|
|
58
|
+
json(
|
|
59
|
+
{ metadata: { id: "u1" }, email: "a@b.co" },
|
|
60
|
+
{ headers: { "set-cookie": "hatchet=sess-abc; Path=/; HttpOnly" } },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const membershipsWith = (slug: string, id: string) =>
|
|
64
|
+
json({ rows: [{ tenant: { metadata: { id }, slug } }] });
|
|
65
|
+
|
|
66
|
+
describe("mintHatchetToken", () => {
|
|
67
|
+
it("registers, finds the seeded default tenant, and mints the token", async () => {
|
|
68
|
+
const { fetchImpl, calls } = fakeHatchet({
|
|
69
|
+
"POST /api/v1/users/register": () => json({ metadata: { id: "u1" } }),
|
|
70
|
+
"POST /api/v1/users/login": loginOk,
|
|
71
|
+
"GET /api/v1/users/memberships": () =>
|
|
72
|
+
membershipsWith("default", "tenant-1"),
|
|
73
|
+
"POST /api/v1/tenants/tenant-1/api-tokens": () =>
|
|
74
|
+
json({ token: "jwt-token-123" }),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await mintHatchetToken({
|
|
78
|
+
url: "https://hatchet.example.com/",
|
|
79
|
+
email: "admin@acme.com",
|
|
80
|
+
password: "Sup3rSecret!!",
|
|
81
|
+
fetchImpl,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
token: "jwt-token-123",
|
|
86
|
+
tenantId: "tenant-1",
|
|
87
|
+
tenantSlug: "default",
|
|
88
|
+
createdTenant: false,
|
|
89
|
+
registered: true,
|
|
90
|
+
});
|
|
91
|
+
// Authed calls carry the session cookie from login.
|
|
92
|
+
const tokenCall = calls.find((c) => c.url.includes("api-tokens"));
|
|
93
|
+
expect(tokenCall?.cookie).toBe("hatchet=sess-abc");
|
|
94
|
+
expect(tokenCall?.body).toEqual({ name: "hogsend" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("falls back to login when signups are disabled (SERVER_ALLOW_SIGNUP=false)", async () => {
|
|
98
|
+
const { fetchImpl } = fakeHatchet({
|
|
99
|
+
"POST /api/v1/users/register": () =>
|
|
100
|
+
json(
|
|
101
|
+
{ errors: [{ description: "user signups are disabled" }] },
|
|
102
|
+
{ status: 400 },
|
|
103
|
+
),
|
|
104
|
+
"POST /api/v1/users/login": loginOk,
|
|
105
|
+
"GET /api/v1/users/memberships": () =>
|
|
106
|
+
membershipsWith("default", "tenant-1"),
|
|
107
|
+
"POST /api/v1/tenants/tenant-1/api-tokens": () =>
|
|
108
|
+
json({ token: "jwt-token-456" }),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const result = await mintHatchetToken({
|
|
112
|
+
url: "https://hatchet.example.com",
|
|
113
|
+
email: "admin@acme.com",
|
|
114
|
+
password: "Sup3rSecret!!",
|
|
115
|
+
fetchImpl,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.token).toBe("jwt-token-456");
|
|
119
|
+
expect(result.registered).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("creates the tenant (engineVersion V1) when the slug has no membership", async () => {
|
|
123
|
+
const { fetchImpl, calls } = fakeHatchet({
|
|
124
|
+
"POST /api/v1/users/register": () =>
|
|
125
|
+
json({ errors: [] }, { status: 400 }),
|
|
126
|
+
"POST /api/v1/users/login": loginOk,
|
|
127
|
+
"GET /api/v1/users/memberships": () => json({ rows: [] }),
|
|
128
|
+
"POST /api/v1/tenants": () =>
|
|
129
|
+
json({ metadata: { id: "tenant-9" }, slug: "hogsend" }),
|
|
130
|
+
"POST /api/v1/tenants/tenant-9/api-tokens": () =>
|
|
131
|
+
json({ token: "jwt-token-789" }),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await mintHatchetToken({
|
|
135
|
+
url: "https://hatchet.example.com",
|
|
136
|
+
email: "admin@acme.com",
|
|
137
|
+
password: "Sup3rSecret!!",
|
|
138
|
+
tenantSlug: "hogsend",
|
|
139
|
+
tokenName: "worker",
|
|
140
|
+
fetchImpl,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result).toMatchObject({
|
|
144
|
+
token: "jwt-token-789",
|
|
145
|
+
tenantId: "tenant-9",
|
|
146
|
+
tenantSlug: "hogsend",
|
|
147
|
+
createdTenant: true,
|
|
148
|
+
});
|
|
149
|
+
const createCall = calls.find((c) => c.url.endsWith("/api/v1/tenants"));
|
|
150
|
+
expect(createCall?.body).toEqual({
|
|
151
|
+
name: "hogsend",
|
|
152
|
+
slug: "hogsend",
|
|
153
|
+
engineVersion: "V1",
|
|
154
|
+
});
|
|
155
|
+
expect(createCall?.cookie).toBe("hatchet=sess-abc");
|
|
156
|
+
const tokenCall = calls.find((c) => c.url.includes("api-tokens"));
|
|
157
|
+
expect(tokenCall?.body).toEqual({ name: "worker" });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("throws a login error (with Hatchet's description) on bad credentials", async () => {
|
|
161
|
+
const { fetchImpl } = fakeHatchet({
|
|
162
|
+
"POST /api/v1/users/register": () =>
|
|
163
|
+
json({ errors: [] }, { status: 400 }),
|
|
164
|
+
"POST /api/v1/users/login": () =>
|
|
165
|
+
json(
|
|
166
|
+
{ errors: [{ description: "invalid email or password" }] },
|
|
167
|
+
{ status: 401 },
|
|
168
|
+
),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
mintHatchetToken({
|
|
173
|
+
url: "https://hatchet.example.com",
|
|
174
|
+
email: "admin@acme.com",
|
|
175
|
+
password: "wrong",
|
|
176
|
+
fetchImpl,
|
|
177
|
+
}),
|
|
178
|
+
).rejects.toThrowError(/login failed \(401\): invalid email or password/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects an invalid base url and an invalid tenant slug without fetching", async () => {
|
|
182
|
+
const { fetchImpl, calls } = fakeHatchet({});
|
|
183
|
+
|
|
184
|
+
await expect(
|
|
185
|
+
mintHatchetToken({
|
|
186
|
+
url: "hatchet.example.com",
|
|
187
|
+
email: "a@b.co",
|
|
188
|
+
password: "x",
|
|
189
|
+
fetchImpl,
|
|
190
|
+
}),
|
|
191
|
+
).rejects.toBeInstanceOf(HatchetTokenError);
|
|
192
|
+
|
|
193
|
+
await expect(
|
|
194
|
+
mintHatchetToken({
|
|
195
|
+
url: "https://hatchet.example.com",
|
|
196
|
+
email: "a@b.co",
|
|
197
|
+
password: "x",
|
|
198
|
+
tenantSlug: "Not A Slug",
|
|
199
|
+
fetchImpl,
|
|
200
|
+
}),
|
|
201
|
+
).rejects.toThrowError(/invalid tenant slug/);
|
|
202
|
+
|
|
203
|
+
expect(calls).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { HatchetTokenError, mintHatchetToken } from "../lib/hatchet-token.js";
|
|
3
|
+
import { color } from "../lib/output.js";
|
|
4
|
+
import type { Command, CommandContext } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `hogsend hatchet token` — mint a HATCHET_CLIENT_TOKEN headlessly against a
|
|
8
|
+
* hatchet-lite instance (register-or-login → ensure tenant → create API token)
|
|
9
|
+
* so the Railway-template "copy the token out of the dashboard" step goes away.
|
|
10
|
+
*
|
|
11
|
+
* Output contract: on success the ONLY thing written to stdout is the token
|
|
12
|
+
* (+ newline) so it pipes cleanly into `railway variables --set` etc. All
|
|
13
|
+
* progress goes to stderr; --json swaps stdout for a single JSON document.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const usage = `hogsend hatchet token [options]
|
|
17
|
+
|
|
18
|
+
Mint a Hatchet API token (HATCHET_CLIENT_TOKEN) headlessly against a
|
|
19
|
+
hatchet-lite instance. Registers the account if the instance still allows
|
|
20
|
+
signups, otherwise logs in (e.g. with the seeded ADMIN_EMAIL/ADMIN_PASSWORD on
|
|
21
|
+
a locked-down deployment), ensures the tenant exists, and creates the token.
|
|
22
|
+
|
|
23
|
+
On success the token is the ONLY thing printed to stdout — pipe it straight
|
|
24
|
+
into your platform's variable store. Progress and errors go to stderr.
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--url <hatchet-url> Hatchet base URL (or HATCHET_URL), e.g. the
|
|
28
|
+
hatchet-lite service's public https URL. Required —
|
|
29
|
+
this command never falls back to HOGSEND_API_URL or
|
|
30
|
+
the localhost default (those target your Hogsend
|
|
31
|
+
API, not Hatchet). NOTE: for this command the global
|
|
32
|
+
--url targets HATCHET, not your Hogsend API.
|
|
33
|
+
--email <e> Account email (or HATCHET_ADMIN_EMAIL).
|
|
34
|
+
--password <p> Account password (or HATCHET_ADMIN_PASSWORD). Prefer
|
|
35
|
+
the env var — flags can leak into shell history.
|
|
36
|
+
--tenant <slug> Tenant slug (default "default", the seeded tenant).
|
|
37
|
+
Created (engine V1) if it doesn't exist yet.
|
|
38
|
+
--token-name <n> Display name for the minted token (default "hogsend").
|
|
39
|
+
--json Emit { token, tenantId, ... } as one JSON document.
|
|
40
|
+
-h, --help Show this help.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
hogsend hatchet token --url https://hatchet-lite-production.up.railway.app \\
|
|
44
|
+
--email admin@example.com --password 'Admin123!!'
|
|
45
|
+
HATCHET_ADMIN_PASSWORD=... hogsend hatchet token --url https://... --email admin@acme.com
|
|
46
|
+
railway variables --service hogsend-worker \\
|
|
47
|
+
--set "HATCHET_CLIENT_TOKEN=$(hogsend hatchet token --url ... --email ... --password ...)"
|
|
48
|
+
|
|
49
|
+
Lockdown note: hatchet-lite ships with OPEN registration — anyone who finds the
|
|
50
|
+
public URL can create an account. On a public deployment set
|
|
51
|
+
SERVER_ALLOW_SIGNUP=false (plus a real ADMIN_EMAIL/ADMIN_PASSWORD, which
|
|
52
|
+
hatchet-lite seeds at boot); this command then logs in with those credentials.`;
|
|
53
|
+
|
|
54
|
+
interface TokenFlags {
|
|
55
|
+
url?: string;
|
|
56
|
+
email?: string;
|
|
57
|
+
password?: string;
|
|
58
|
+
tenant?: string;
|
|
59
|
+
tokenName?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseTokenFlags(argv: string[]): TokenFlags {
|
|
63
|
+
const { values } = parseArgs({
|
|
64
|
+
args: argv,
|
|
65
|
+
allowPositionals: true,
|
|
66
|
+
strict: false,
|
|
67
|
+
options: {
|
|
68
|
+
url: { type: "string" },
|
|
69
|
+
email: { type: "string" },
|
|
70
|
+
password: { type: "string" },
|
|
71
|
+
tenant: { type: "string" },
|
|
72
|
+
"token-name": { type: "string" },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const str = (v: unknown): string | undefined =>
|
|
76
|
+
typeof v === "string" && v.length > 0 ? v : undefined;
|
|
77
|
+
return {
|
|
78
|
+
url: str(values.url),
|
|
79
|
+
email: str(values.email),
|
|
80
|
+
password: str(values.password),
|
|
81
|
+
tenant: str(values.tenant),
|
|
82
|
+
tokenName: str(values["token-name"]),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Terminal failure that honors the stdout-is-only-the-token contract: human
|
|
88
|
+
* mode always writes to stderr (even in a TTY — clack's cancel would land on
|
|
89
|
+
* stdout); --json keeps the single-JSON-document contract via out.fail.
|
|
90
|
+
*/
|
|
91
|
+
function failToStderr(ctx: CommandContext, message: string): never {
|
|
92
|
+
if (ctx.json) {
|
|
93
|
+
ctx.out.fail(message);
|
|
94
|
+
}
|
|
95
|
+
process.stderr.write(`${color.red("error")} ${message}\n`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function runToken(ctx: CommandContext, argv: string[]): Promise<void> {
|
|
100
|
+
const flags = parseTokenFlags(argv);
|
|
101
|
+
|
|
102
|
+
// The router owns the global `--url` and resolves it into cfg.baseUrl before
|
|
103
|
+
// this command sees argv — an explicit `--url <hatchet-url>` arrives as
|
|
104
|
+
// cfg.baseUrl with cfg.urlExplicit set. We deliberately do NOT fall back to
|
|
105
|
+
// cfg.baseUrl otherwise: it ALWAYS resolves (HOGSEND_API_URL env/.env, then
|
|
106
|
+
// localhost:3002), which would silently POST the Hatchet admin credentials
|
|
107
|
+
// to the Hogsend API with a misleading login error. Without an explicit
|
|
108
|
+
// --url, HATCHET_URL is required and the missing-url error below fires. The
|
|
109
|
+
// local flag parse is kept first for safety should the router ever stop
|
|
110
|
+
// owning --url.
|
|
111
|
+
const url =
|
|
112
|
+
flags.url ??
|
|
113
|
+
(ctx.cfg.urlExplicit ? ctx.cfg.baseUrl : undefined) ??
|
|
114
|
+
process.env.HATCHET_URL;
|
|
115
|
+
const email = flags.email ?? process.env.HATCHET_ADMIN_EMAIL;
|
|
116
|
+
const password = flags.password ?? process.env.HATCHET_ADMIN_PASSWORD;
|
|
117
|
+
|
|
118
|
+
const missing: string[] = [];
|
|
119
|
+
if (!url) missing.push("--url (or HATCHET_URL)");
|
|
120
|
+
if (!email) missing.push("--email (or HATCHET_ADMIN_EMAIL)");
|
|
121
|
+
if (!password) missing.push("--password (or HATCHET_ADMIN_PASSWORD)");
|
|
122
|
+
if (missing.length > 0 || !url || !email || !password) {
|
|
123
|
+
failToStderr(ctx, `missing ${missing.join(", ")}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Progress goes to stderr ONLY — stdout is reserved for the token.
|
|
127
|
+
const onProgress = ctx.json
|
|
128
|
+
? undefined
|
|
129
|
+
: (msg: string) => process.stderr.write(`${color.dim(msg)}\n`);
|
|
130
|
+
onProgress?.(`hatchet: ${url}`);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = await mintHatchetToken({
|
|
134
|
+
url,
|
|
135
|
+
email,
|
|
136
|
+
password,
|
|
137
|
+
tenantSlug: flags.tenant,
|
|
138
|
+
tokenName: flags.tokenName,
|
|
139
|
+
onProgress,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (ctx.json) {
|
|
143
|
+
ctx.out.json(result);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(`${result.token}\n`);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof HatchetTokenError) {
|
|
149
|
+
failToStderr(ctx, err.message);
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function run(ctx: CommandContext): Promise<void> {
|
|
156
|
+
const sub = ctx.argv[0];
|
|
157
|
+
const rest = ctx.argv.slice(1);
|
|
158
|
+
|
|
159
|
+
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
160
|
+
ctx.out.log(usage);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (rest.includes("-h") || rest.includes("--help")) {
|
|
164
|
+
ctx.out.log(usage);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
switch (sub) {
|
|
169
|
+
case "token":
|
|
170
|
+
return runToken(ctx, rest);
|
|
171
|
+
default:
|
|
172
|
+
failToStderr(ctx, `unknown subcommand "${sub}" — expected token`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const hatchetCommand: Command = {
|
|
177
|
+
name: "hatchet",
|
|
178
|
+
summary: "Hatchet helpers — mint a HATCHET_CLIENT_TOKEN headlessly",
|
|
179
|
+
usage,
|
|
180
|
+
run,
|
|
181
|
+
};
|
package/src/commands/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { domainCommand } from "./domain.js";
|
|
|
6
6
|
import { ejectCommand } from "./eject.js";
|
|
7
7
|
import { emailsCommand } from "./emails.js";
|
|
8
8
|
import { eventsCommand } from "./events.js";
|
|
9
|
+
import { hatchetCommand } from "./hatchet.js";
|
|
9
10
|
import { journeysCommand } from "./journeys.js";
|
|
10
11
|
import { patchCommand } from "./patch.js";
|
|
11
12
|
import { setupCommand } from "./setup.js";
|
|
@@ -34,6 +35,7 @@ export const commands: Command[] = [
|
|
|
34
35
|
campaignsCommand,
|
|
35
36
|
webhooksCommand,
|
|
36
37
|
domainCommand,
|
|
38
|
+
hatchetCommand,
|
|
37
39
|
studioCommand,
|
|
38
40
|
devCommand,
|
|
39
41
|
setupCommand,
|
package/src/lib/config.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface ResolvedConfig {
|
|
|
14
14
|
* to the admin key since `full-admin` implies `ingest`.
|
|
15
15
|
*/
|
|
16
16
|
dataKey: string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* True when `baseUrl` came from an explicit `--url` flag (vs env / .env /
|
|
19
|
+
* the localhost default). Commands that target a DIFFERENT host than the
|
|
20
|
+
* Hogsend API (e.g. `hatchet token`) must only honor `baseUrl` when this is
|
|
21
|
+
* set, so a cwd `.env`'s HOGSEND_API_URL can't silently become their target.
|
|
22
|
+
*/
|
|
23
|
+
urlExplicit: boolean;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/** Global flags parsed off the front of any command's argv. */
|
|
@@ -165,5 +172,6 @@ export function resolveConfig(
|
|
|
165
172
|
baseUrl: baseUrlRaw.replace(/\/+$/, ""),
|
|
166
173
|
adminKey: adminKey && adminKey.length > 0 ? adminKey : undefined,
|
|
167
174
|
dataKey: dataKey && dataKey.length > 0 ? dataKey : undefined,
|
|
175
|
+
urlExplicit: flags.url !== undefined,
|
|
168
176
|
};
|
|
169
177
|
}
|