@sendpanda/cli 0.1.0 → 0.2.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/README.md +1 -1
- package/dist/cli.js +70 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
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
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx @sendpanda/cli login
|
|
6
|
+
npx @sendpanda/cli login # opens your browser to approve access (or: login --token sp_pat_...)
|
|
7
7
|
npx @sendpanda/cli whoami # accounts, projects, environment ids
|
|
8
8
|
npx @sendpanda/cli setup --project "My App" --domain example.com --env-file .env
|
|
9
9
|
```
|
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { dirname, join } from "path";
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { spawn } from "child_process";
|
|
7
10
|
var DEFAULT_API = "https://api.sendpanda.email";
|
|
11
|
+
var DEFAULT_WEB = "https://sendpanda.email";
|
|
8
12
|
var HELP = `sendpanda: manage SendPanda from a terminal or a coding agent
|
|
9
13
|
|
|
10
14
|
Setup
|
|
11
|
-
login --
|
|
15
|
+
login [--api URL] [--web URL] Open SendPanda in your browser to approve access
|
|
16
|
+
login --token <sp_pat_...> Save an access token instead (Settings > Connections)
|
|
12
17
|
logout Forget the saved token
|
|
13
18
|
whoami Show your accounts, projects and environment ids
|
|
14
19
|
setup --project NAME --domain DOMAIN Create a project, add the domain, create a sending key
|
|
@@ -60,7 +65,8 @@ async function main(argv, deps = {}) {
|
|
|
60
65
|
env: deps.env ?? process.env,
|
|
61
66
|
out: deps.out ?? ((t) => process.stdout.write(t)),
|
|
62
67
|
err: deps.err ?? ((t) => process.stderr.write(t)),
|
|
63
|
-
readFile: deps.readFile ?? ((p) => readFile(p, "utf8"))
|
|
68
|
+
readFile: deps.readFile ?? ((p) => readFile(p, "utf8")),
|
|
69
|
+
open: deps.open ?? openBrowser
|
|
64
70
|
};
|
|
65
71
|
try {
|
|
66
72
|
await run(argv, d);
|
|
@@ -71,6 +77,60 @@ async function main(argv, deps = {}) {
|
|
|
71
77
|
return e instanceof CliError ? 2 : 1;
|
|
72
78
|
}
|
|
73
79
|
}
|
|
80
|
+
function openBrowser(url) {
|
|
81
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url.replace(/&/g, "^&")]] : ["xdg-open", [url]];
|
|
82
|
+
try {
|
|
83
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => {
|
|
84
|
+
}).unref();
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function browserLogin(d, webUrl) {
|
|
89
|
+
const state = randomBytes(16).toString("hex");
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const server = createServer((req, res) => {
|
|
92
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
93
|
+
if (url.pathname !== "/callback") {
|
|
94
|
+
res.writeHead(404).end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const token = url.searchParams.get("token");
|
|
98
|
+
const ok = url.searchParams.get("state") === state && !!token;
|
|
99
|
+
res.writeHead(ok ? 200 : 400, {
|
|
100
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
101
|
+
"Referrer-Policy": "no-referrer"
|
|
102
|
+
}).end(
|
|
103
|
+
ok ? `<!doctype html><title>SendPanda</title><body style="font-family:system-ui;padding:3rem"><h2>You're signed in.</h2><p>You can close this tab and return to your terminal.</p>` : '<!doctype html><title>SendPanda</title><body style="font-family:system-ui;padding:3rem"><h2>Sign-in failed.</h2><p>Return to your terminal and run sendpanda login again.</p>'
|
|
104
|
+
);
|
|
105
|
+
if (ok) done(() => resolve(token));
|
|
106
|
+
});
|
|
107
|
+
const timer = setTimeout(
|
|
108
|
+
() => done(
|
|
109
|
+
() => reject(new CliError("Timed out waiting for browser approval"))
|
|
110
|
+
),
|
|
111
|
+
5 * 6e4
|
|
112
|
+
);
|
|
113
|
+
function done(fn) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
server.close();
|
|
116
|
+
server.closeAllConnections();
|
|
117
|
+
fn();
|
|
118
|
+
}
|
|
119
|
+
server.on("error", (e) => done(() => reject(e)));
|
|
120
|
+
server.listen(0, "127.0.0.1", () => {
|
|
121
|
+
const port = server.address().port;
|
|
122
|
+
const target = new URL("/cli/authorize", webUrl);
|
|
123
|
+
target.searchParams.set("port", String(port));
|
|
124
|
+
target.searchParams.set("state", state);
|
|
125
|
+
d.err(
|
|
126
|
+
`Opening your browser to approve access. If it does not open, visit:
|
|
127
|
+
${target}
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
d.open(target.toString());
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
74
134
|
function configPath(d) {
|
|
75
135
|
return join(
|
|
76
136
|
d.env.SENDPANDA_CONFIG_DIR ?? join(homedir(), ".config", "sendpanda"),
|
|
@@ -108,10 +168,7 @@ async function run(argv, d) {
|
|
|
108
168
|
const print = (value) => d.out(JSON.stringify(value, null, 2) + "\n");
|
|
109
169
|
async function call(method, path, options = {}) {
|
|
110
170
|
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
|
-
);
|
|
171
|
+
if (!secret) throw new CliError("Not signed in. Run: sendpanda login");
|
|
115
172
|
if (!/^\/(v1|api)\//.test(path))
|
|
116
173
|
throw new CliError("Path must start with /v1/ or /api/");
|
|
117
174
|
const response = await d.fetch(new URL(path, base), {
|
|
@@ -175,11 +232,13 @@ async function run(argv, d) {
|
|
|
175
232
|
};
|
|
176
233
|
switch (command) {
|
|
177
234
|
case "login": {
|
|
178
|
-
|
|
179
|
-
if (!given
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
235
|
+
let given = str(flags.token) ?? d.env.SENDPANDA_TOKEN;
|
|
236
|
+
if (!given) {
|
|
237
|
+
const web = str(flags.web) ?? d.env.SENDPANDA_WEB_URL ?? (base.hostname.startsWith("api.") ? `${base.protocol}//${base.host.slice(4)}` : base.hostname === "localhost" || base.hostname === "127.0.0.1" ? "http://localhost:3000" : DEFAULT_WEB);
|
|
238
|
+
given = await browserLogin(d, web);
|
|
239
|
+
}
|
|
240
|
+
if (!given.startsWith("sp_pat_"))
|
|
241
|
+
throw new CliError("That is not a SendPanda access token (sp_pat_...)");
|
|
183
242
|
const who = await call("GET", "/api/me", { token: given });
|
|
184
243
|
await saveConfig(d, {
|
|
185
244
|
...saved,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendpanda/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "SendPanda command line: manage every project, domain, key, template and email from a terminal or a coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"private": false,
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bin": {
|
|
12
|
-
"sendpanda": "
|
|
12
|
+
"sendpanda": "dist/cli.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|