@sendpanda/cli 0.0.0-stage → 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -2
  3. package/dist/cli.js +462 -0
  4. package/package.json +38 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dilhan Geeth Amarasinghe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
1
+ # SendPanda CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Manage every SendPanda project from a terminal or a coding agent: create projects, add and verify domains, create keys and templates, send email. Requires Node.js 22 or newer.
4
+
5
+ ```sh
6
+ npx @sendpanda/cli login --token sp_pat_... # token: dashboard → Settings → Connections → New token
7
+ npx @sendpanda/cli whoami # accounts, projects, environment ids
8
+ npx @sendpanda/cli setup --project "My App" --domain example.com --env-file .env
9
+ ```
10
+
11
+ `setup` creates the project (or reuses it), adds the domain and prints the DNS records to publish, and creates a **send-only** API key (appended to `--env-file`, or printed once). After the records are live: `sendpanda domains verify <id>`.
12
+
13
+ ## Commands
14
+
15
+ Run `sendpanda help` for the full list. Everything prints JSON on stdout (messages go to stderr), so agents can parse it.
16
+
17
+ | Area | Commands |
18
+ | ------------- | ----------------------------------------------------------- |
19
+ | Session | `login`, `logout`, `whoami`, `use PROJECT` |
20
+ | Projects | `projects list / create / rename / delete --confirm "NAME"` |
21
+ | Domains | `domains list / add / get / verify / delete` |
22
+ | Keys | `keys list / create / revoke` |
23
+ | Templates | `templates list / get / create / publish / preview` |
24
+ | Email | `emails send / list / get` |
25
+ | Webhooks | `webhooks list / create / delete` |
26
+ | Anything else | `api METHOD PATH --data JSON`, `routes [FILTER]` |
27
+
28
+ Pass `--env PROJECT` (a name, project id or environment id) to act on a project other than the one chosen with `use`.
29
+
30
+ ## Access tokens
31
+
32
+ An access token acts as you, with exactly your permissions, on every project you can reach. It cannot manage billing, sign-in, or other tokens: those need the dashboard. Tokens expire (default 90 days) and can be revoked in Settings → Connections. Treat one like a password; `SENDPANDA_TOKEN` overrides the saved login, and the saved file is readable only by you.
33
+
34
+ Deleting a project is permanent and requires its exact name.
35
+
36
+ ## MCP
37
+
38
+ The same token powers an MCP server covering all projects:
39
+
40
+ ```sh
41
+ SENDPANDA_TOKEN=sp_pat_... npx -y @sendpanda/mcp
42
+ ```
43
+
44
+ See `docs/agents.md` in the repository for setting up an agent end to end.
package/dist/cli.js ADDED
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { mkdir, readFile, writeFile, chmod } from "fs/promises";
5
+ import { homedir } from "os";
6
+ import { dirname, join } from "path";
7
+ var DEFAULT_API = "https://api.sendpanda.email";
8
+ var HELP = `sendpanda: manage SendPanda from a terminal or a coding agent
9
+
10
+ Setup
11
+ login --token <sp_pat_...> [--api URL] Save an access token (Settings > Connections > New token)
12
+ logout Forget the saved token
13
+ whoami Show your accounts, projects and environment ids
14
+ setup --project NAME --domain DOMAIN Create a project, add the domain, create a sending key
15
+ [--account ORG_ID] [--env-file .env]
16
+
17
+ Projects (accounts hold projects; each project has environments such as Production)
18
+ projects list
19
+ projects create NAME [--account ORG_ID]
20
+ projects rename PROJECT NEW_NAME
21
+ projects delete PROJECT --confirm "EXACT NAME" PERMANENT, with all its data
22
+ use PROJECT Default environment for later commands
23
+
24
+ Resources (use --env PROJECT_OR_ENVIRONMENT to override the default)
25
+ domains list | add NAME | get ID | verify ID | delete ID
26
+ keys list | create NAME [--scope send|full] | revoke ID
27
+ templates list | get ID | create --name N --subject S (--html-file F | --text-file F | --design-file F)
28
+ | publish ID [--version N] | preview ID
29
+ emails send --from A --to B[,C] --subject S (--html-file F | --text-file F | --template ID) | list | get ID
30
+ webhooks list | create URL [--events a,b] | delete ID
31
+
32
+ Everything else
33
+ api METHOD PATH [--data JSON|@file] [--env ...] Any API endpoint as you
34
+ routes [FILTER] List API endpoints
35
+ mcp Run the MCP server for all your projects
36
+
37
+ Output is JSON on stdout; messages go to stderr. SENDPANDA_TOKEN, SENDPANDA_API_URL and
38
+ SENDPANDA_ENVIRONMENT override the saved login. Billing, sign-in and tokens need the dashboard.
39
+ `;
40
+ var CliError = class extends Error {
41
+ };
42
+ function parse(argv) {
43
+ const positional = [], flags = {};
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const a = argv[i];
46
+ if (a.startsWith("--")) {
47
+ const [k, inline] = a.slice(2).split(/=(.*)/s, 2);
48
+ if (inline !== void 0) flags[k] = inline;
49
+ else if (argv[i + 1] !== void 0 && !argv[i + 1].startsWith("--"))
50
+ flags[k] = argv[++i];
51
+ else flags[k] = true;
52
+ } else positional.push(a);
53
+ }
54
+ return { positional, flags };
55
+ }
56
+ var str = (v) => typeof v === "string" ? v : void 0;
57
+ async function main(argv, deps = {}) {
58
+ const d = {
59
+ fetch: deps.fetch ?? fetch,
60
+ env: deps.env ?? process.env,
61
+ out: deps.out ?? ((t) => process.stdout.write(t)),
62
+ err: deps.err ?? ((t) => process.stderr.write(t)),
63
+ readFile: deps.readFile ?? ((p) => readFile(p, "utf8"))
64
+ };
65
+ try {
66
+ await run(argv, d);
67
+ return 0;
68
+ } catch (e) {
69
+ d.err(`${e instanceof Error ? e.message : String(e)}
70
+ `);
71
+ return e instanceof CliError ? 2 : 1;
72
+ }
73
+ }
74
+ function configPath(d) {
75
+ return join(
76
+ d.env.SENDPANDA_CONFIG_DIR ?? join(homedir(), ".config", "sendpanda"),
77
+ "config.json"
78
+ );
79
+ }
80
+ async function loadConfig(d) {
81
+ try {
82
+ return JSON.parse(await d.readFile(configPath(d)));
83
+ } catch {
84
+ return {};
85
+ }
86
+ }
87
+ async function saveConfig(d, config) {
88
+ const path = configPath(d);
89
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
90
+ await writeFile(path, JSON.stringify(config, null, 2) + "\n", {
91
+ mode: 384
92
+ });
93
+ await chmod(path, 384);
94
+ }
95
+ async function run(argv, d) {
96
+ const { positional: p, flags } = parse(argv);
97
+ const [command, sub, ...rest] = p;
98
+ if (!command || command === "help" || flags.help || flags.h) {
99
+ d.out(HELP);
100
+ return;
101
+ }
102
+ const saved = await loadConfig(d);
103
+ const token = d.env.SENDPANDA_TOKEN ?? saved.token;
104
+ const api = (str(flags.api) ?? d.env.SENDPANDA_API_URL ?? saved.api ?? DEFAULT_API).replace(/\/(v1)?\/?$/, "");
105
+ const base = new URL(api);
106
+ if (base.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(base.hostname))
107
+ throw new CliError("The API URL must use HTTPS");
108
+ const print = (value) => d.out(JSON.stringify(value, null, 2) + "\n");
109
+ async function call(method, path, options = {}) {
110
+ const secret = options.token ?? token;
111
+ if (!secret)
112
+ throw new CliError(
113
+ "Not signed in. Create an access token in the dashboard (Settings > Connections) and run: sendpanda login --token sp_pat_..."
114
+ );
115
+ if (!/^\/(v1|api)\//.test(path))
116
+ throw new CliError("Path must start with /v1/ or /api/");
117
+ const response = await d.fetch(new URL(path, base), {
118
+ method,
119
+ headers: {
120
+ Authorization: `Bearer ${secret}`,
121
+ ...options.env ? { "X-Environment-Id": options.env } : {},
122
+ ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
123
+ },
124
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
125
+ redirect: "error",
126
+ signal: AbortSignal.timeout(3e4)
127
+ });
128
+ const data = await response.json().catch(() => null);
129
+ if (!response.ok)
130
+ throw new Error(
131
+ `${data?.error?.code ?? "request_failed"}: ${data?.error?.message ?? `HTTP ${response.status}`}`
132
+ );
133
+ return data;
134
+ }
135
+ const me = () => call("GET", "/api/me");
136
+ function findProject(all, value) {
137
+ const lower = value.toLowerCase();
138
+ const application = all.applications.find((a) => a.id === value) ?? all.applications.find((a) => a.name.toLowerCase() === lower);
139
+ const environment2 = all.projects.find((e) => e.id === value) ?? (application ? all.projects.find(
140
+ (e) => e.application_id === application.id && e.name === "Production"
141
+ ) ?? all.projects.find((e) => e.application_id === application.id) : void 0);
142
+ const owner = application ?? all.applications.find((a) => a.id === environment2?.application_id);
143
+ if (!owner)
144
+ throw new CliError(
145
+ `No project matches "${value}". Run: sendpanda projects list`
146
+ );
147
+ return { application: owner, environment: environment2 };
148
+ }
149
+ async function environment() {
150
+ const wanted = str(flags.env) ?? d.env.SENDPANDA_ENVIRONMENT ?? saved.environment;
151
+ if (!wanted)
152
+ throw new CliError(
153
+ "Choose a project: sendpanda use PROJECT, or pass --env PROJECT"
154
+ );
155
+ if (/^prj_[a-f0-9]+$/.test(wanted)) return wanted;
156
+ const found = findProject(await me(), wanted).environment;
157
+ if (!found) throw new CliError(`Project "${wanted}" has no environment`);
158
+ return found.id;
159
+ }
160
+ const inEnv = async (method, path, body) => call(method, path, { body, env: await environment() });
161
+ const content = async () => {
162
+ const out = {};
163
+ for (const [flag, key] of [
164
+ ["html-file", "html"],
165
+ ["text-file", "text"]
166
+ ])
167
+ if (str(flags[flag])) out[key] = await d.readFile(str(flags[flag]));
168
+ if (str(flags["design-file"]))
169
+ out.design = JSON.parse(await d.readFile(str(flags["design-file"])));
170
+ return out;
171
+ };
172
+ const need = (value, what) => {
173
+ if (!value) throw new CliError(`Missing ${what}. See: sendpanda help`);
174
+ return value;
175
+ };
176
+ switch (command) {
177
+ case "login": {
178
+ const given = str(flags.token) ?? d.env.SENDPANDA_TOKEN;
179
+ if (!given?.startsWith("sp_pat_"))
180
+ throw new CliError(
181
+ "Pass an access token: sendpanda login --token sp_pat_... (create one in the dashboard under Settings > Connections)"
182
+ );
183
+ const who = await call("GET", "/api/me", { token: given });
184
+ await saveConfig(d, {
185
+ ...saved,
186
+ token: given,
187
+ ...str(flags.api) ? { api: str(flags.api) } : {}
188
+ });
189
+ d.err(`Signed in as ${who.user.email}
190
+ `);
191
+ print({ user: who.user });
192
+ return;
193
+ }
194
+ case "logout":
195
+ await saveConfig(d, { ...saved, token: void 0 });
196
+ d.err("Signed out. Revoke the token in the dashboard to disable it.\n");
197
+ return;
198
+ case "whoami": {
199
+ const all = await me();
200
+ print({
201
+ user: all.user,
202
+ accounts: all.organisations,
203
+ projects: all.applications.map((a) => ({
204
+ ...a,
205
+ environments: all.projects.filter((e) => e.application_id === a.id).map((e) => ({ id: e.id, name: e.name }))
206
+ })),
207
+ default_environment: d.env.SENDPANDA_ENVIRONMENT ?? saved.environment ?? null
208
+ });
209
+ return;
210
+ }
211
+ case "use": {
212
+ const found = findProject(await me(), need(sub, "project"));
213
+ if (!found.environment) throw new CliError("Project has no environment");
214
+ await saveConfig(d, { ...saved, environment: found.environment.id });
215
+ d.err(`Using ${found.application.name} (${found.environment.id})
216
+ `);
217
+ print({ environment: found.environment.id });
218
+ return;
219
+ }
220
+ case "projects": {
221
+ if (sub === "list" || !sub) {
222
+ const all = await me();
223
+ print(
224
+ all.applications.map((a) => ({
225
+ ...a,
226
+ environments: all.projects.filter((e) => e.application_id === a.id).map((e) => ({ id: e.id, name: e.name }))
227
+ }))
228
+ );
229
+ } else if (sub === "create") {
230
+ const all = await me();
231
+ const account = str(flags.account) ?? (all.organisations.length === 1 ? all.organisations[0].id : void 0);
232
+ if (!account)
233
+ throw new CliError(
234
+ "Pass --account ORG_ID (you belong to several accounts; see: sendpanda whoami)"
235
+ );
236
+ print(
237
+ await call("POST", "/api/projects", {
238
+ body: {
239
+ name: need(rest[0], "project name"),
240
+ organisation_id: account
241
+ }
242
+ })
243
+ );
244
+ } else if (sub === "rename") {
245
+ const found = findProject(await me(), need(rest[0], "project"));
246
+ print(
247
+ await call("PATCH", `/api/projects/${found.application.id}`, {
248
+ body: { name: need(rest[1], "new name") }
249
+ })
250
+ );
251
+ } else if (sub === "delete") {
252
+ const found = findProject(await me(), need(rest[0], "project"));
253
+ if (str(flags.confirm) !== found.application.name)
254
+ throw new CliError(
255
+ `This permanently deletes "${found.application.name}" and all its data. Re-run with --confirm "${found.application.name}"`
256
+ );
257
+ print(
258
+ await call("POST", `/api/projects/${found.application.id}/purge`, {
259
+ body: { confirm_name: found.application.name }
260
+ })
261
+ );
262
+ } else throw new CliError("Usage: projects list|create|rename|delete");
263
+ return;
264
+ }
265
+ case "domains": {
266
+ if (sub === "list" || !sub) print(await inEnv("GET", "/v1/domains"));
267
+ else if (sub === "add")
268
+ print(
269
+ await inEnv("POST", "/v1/domains", { name: need(rest[0], "domain") })
270
+ );
271
+ else if (sub === "get")
272
+ print(await inEnv("GET", `/v1/domains/${need(rest[0], "domain id")}`));
273
+ else if (sub === "verify")
274
+ print(
275
+ await inEnv(
276
+ "POST",
277
+ `/v1/domains/${need(rest[0], "domain id")}/verify`
278
+ )
279
+ );
280
+ else if (sub === "delete")
281
+ print(
282
+ await inEnv("DELETE", `/v1/domains/${need(rest[0], "domain id")}`)
283
+ );
284
+ else throw new CliError("Usage: domains list|add|get|verify|delete");
285
+ return;
286
+ }
287
+ case "keys": {
288
+ if (sub === "list" || !sub) print(await inEnv("GET", "/v1/api-keys"));
289
+ else if (sub === "create")
290
+ print(
291
+ await inEnv("POST", "/v1/api-keys", {
292
+ name: need(rest[0], "key name"),
293
+ scope: str(flags.scope) ?? "send"
294
+ })
295
+ );
296
+ else if (sub === "revoke")
297
+ print(await inEnv("DELETE", `/v1/api-keys/${need(rest[0], "key id")}`));
298
+ else throw new CliError("Usage: keys list|create|revoke");
299
+ return;
300
+ }
301
+ case "templates": {
302
+ if (sub === "list" || !sub) print(await inEnv("GET", "/v1/templates"));
303
+ else if (sub === "get")
304
+ print(
305
+ await inEnv("GET", `/v1/templates/${need(rest[0], "template id")}`)
306
+ );
307
+ else if (sub === "create")
308
+ print(
309
+ await inEnv("POST", "/v1/templates", {
310
+ name: need(str(flags.name), "--name"),
311
+ subject: need(str(flags.subject), "--subject"),
312
+ ...await content()
313
+ })
314
+ );
315
+ else if (sub === "publish") {
316
+ const id = need(rest[0], "template id");
317
+ let version = Number(str(flags.version));
318
+ if (!version) {
319
+ const t = await inEnv("GET", `/v1/templates/${id}`);
320
+ version = Math.max(...(t.versions ?? []).map((v) => v.version), 0);
321
+ }
322
+ if (!version) throw new CliError("Template has no version to publish");
323
+ print(await inEnv("POST", `/v1/templates/${id}/publish`, { version }));
324
+ } else if (sub === "preview")
325
+ print(
326
+ await inEnv(
327
+ "POST",
328
+ `/v1/templates/${need(rest[0], "template id")}/preview`,
329
+ { variables: {} }
330
+ )
331
+ );
332
+ else
333
+ throw new CliError("Usage: templates list|get|create|publish|preview");
334
+ return;
335
+ }
336
+ case "emails": {
337
+ if (sub === "list" || !sub) print(await inEnv("GET", "/v1/emails"));
338
+ else if (sub === "get")
339
+ print(await inEnv("GET", `/v1/emails/${need(rest[0], "email id")}`));
340
+ else if (sub === "send")
341
+ print(
342
+ await inEnv("POST", "/v1/emails", {
343
+ from: need(str(flags.from), "--from"),
344
+ to: need(str(flags.to), "--to").split(",").map((v) => v.trim()),
345
+ ...str(flags.subject) ? { subject: str(flags.subject) } : {},
346
+ ...str(flags.template) ? { template_id: str(flags.template) } : {},
347
+ ...await content()
348
+ })
349
+ );
350
+ else throw new CliError("Usage: emails send|list|get");
351
+ return;
352
+ }
353
+ case "webhooks": {
354
+ if (sub === "list" || !sub) print(await inEnv("GET", "/v1/webhooks"));
355
+ else if (sub === "create")
356
+ print(
357
+ await inEnv("POST", "/v1/webhooks", {
358
+ url: need(rest[0], "URL"),
359
+ events: (str(flags.events) ?? "email.delivered,email.bounced,email.complained,email.failed").split(",")
360
+ })
361
+ );
362
+ else if (sub === "delete")
363
+ print(
364
+ await inEnv("DELETE", `/v1/webhooks/${need(rest[0], "webhook id")}`)
365
+ );
366
+ else throw new CliError("Usage: webhooks list|create|delete");
367
+ return;
368
+ }
369
+ case "api": {
370
+ const method = need(sub, "METHOD").toUpperCase();
371
+ if (!["GET", "POST", "PATCH", "DELETE"].includes(method))
372
+ throw new CliError("METHOD must be GET, POST, PATCH or DELETE");
373
+ let body;
374
+ const data = str(flags.data);
375
+ if (data !== void 0)
376
+ body = JSON.parse(
377
+ data.startsWith("@") ? await d.readFile(data.slice(1)) : data
378
+ );
379
+ const wantsEnv = str(flags.env) || (rest[0] ?? "").startsWith("/v1/");
380
+ print(
381
+ await call(method, need(rest[0], "PATH"), {
382
+ body,
383
+ env: wantsEnv ? await environment() : void 0
384
+ })
385
+ );
386
+ return;
387
+ }
388
+ case "routes": {
389
+ const spec = await d.fetch(new URL("/openapi.json", base)).then((r) => r.json());
390
+ const filter = (sub ?? "").toLowerCase();
391
+ print(
392
+ Object.entries(spec.paths).flatMap(
393
+ ([path, methods]) => Object.keys(methods).filter(() => path.toLowerCase().includes(filter)).map((m) => `${m.toUpperCase()} ${path}`)
394
+ )
395
+ );
396
+ return;
397
+ }
398
+ case "setup": {
399
+ const name = need(str(flags.project), "--project NAME");
400
+ const domain = need(str(flags.domain), "--domain DOMAIN");
401
+ const all = await me();
402
+ let found = all.applications.find(
403
+ (a) => a.name.toLowerCase() === name.toLowerCase()
404
+ );
405
+ let environmentId = found ? all.projects.find(
406
+ (e) => e.application_id === found.id && e.name === "Production"
407
+ )?.id : void 0;
408
+ if (!found) {
409
+ const account = str(flags.account) ?? (all.organisations.length === 1 ? all.organisations[0].id : void 0);
410
+ if (!account)
411
+ throw new CliError("Pass --account ORG_ID (see: sendpanda whoami)");
412
+ const created = await call("POST", "/api/projects", {
413
+ body: { name, organisation_id: account }
414
+ });
415
+ environmentId = created.environment_id;
416
+ d.err(`Created project ${name}
417
+ `);
418
+ } else d.err(`Using existing project ${found.name}
419
+ `);
420
+ if (!environmentId)
421
+ throw new CliError("Project has no Production environment");
422
+ const env = environmentId;
423
+ const domains = await call("GET", "/v1/domains", { env });
424
+ const existing = domains.data.find(
425
+ (x) => x.name === domain.toLowerCase()
426
+ );
427
+ const added = existing ? await call("GET", `/v1/domains/${existing.id}`, { env }) : await call("POST", "/v1/domains", { env, body: { name: domain } });
428
+ const key = await call("POST", "/v1/api-keys", {
429
+ env,
430
+ body: { name: "Created by sendpanda setup", scope: "send" }
431
+ });
432
+ const secret = key.secret ?? key.key;
433
+ if (str(flags["env-file"]) && secret)
434
+ await writeFile(
435
+ str(flags["env-file"]),
436
+ `SENDPANDA_API_KEY=${secret}
437
+ `,
438
+ { flag: "a", mode: 384 }
439
+ );
440
+ await saveConfig(d, { ...saved, environment: env });
441
+ d.err(
442
+ "Add the DNS records below at your DNS provider, then run: sendpanda domains verify <id>\n" + (str(flags["env-file"]) ? `The sending key was appended to ${str(flags["env-file"])}.
443
+ ` : "The sending key is shown once below; store it in your secrets manager.\n")
444
+ );
445
+ print({
446
+ environment: env,
447
+ domain: added,
448
+ sending_key: str(flags["env-file"]) ? "(written to env file)" : secret
449
+ });
450
+ return;
451
+ }
452
+ case "mcp":
453
+ throw new CliError(
454
+ "Run the MCP server with: SENDPANDA_TOKEN=sp_pat_... npx -y @sendpanda/mcp"
455
+ );
456
+ default:
457
+ throw new CliError(`Unknown command "${command}". Run: sendpanda help`);
458
+ }
459
+ }
460
+
461
+ // src/cli.ts
462
+ process.exitCode = await main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@sendpanda/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "SendPanda command line: manage every project, domain, key, template and email from a terminal or a coding agent.",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "private": false,
10
+ "type": "module",
11
+ "bin": {
12
+ "sendpanda": "./dist/cli.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/TheBugEater/SendPanda.git",
22
+ "directory": "packages/cli"
23
+ },
24
+ "homepage": "https://sendpanda.email/docs",
25
+ "keywords": [
26
+ "sendpanda",
27
+ "email",
28
+ "cli",
29
+ "agent"
30
+ ],
31
+ "dependencies": {},
32
+ "scripts": {
33
+ "build": "tsup src/cli.ts --format esm --clean",
34
+ "prepack": "npm run build"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org/"
39
+ }
40
+ }