run402 1.20.1 → 1.22.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/lib/email.mjs +34 -10
- package/lib/functions.mjs +95 -3
- package/package.json +1 -1
package/lib/email.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { findProject, resolveProjectId, API, updateProject, loadKeyStore, saveKeyStore } from "./config.mjs";
|
|
2
2
|
|
|
3
|
-
const HELP = `run402 email — Send
|
|
3
|
+
const HELP = `run402 email — Send emails from your project
|
|
4
4
|
|
|
5
5
|
Usage:
|
|
6
6
|
run402 email <subcommand> [args...]
|
|
@@ -8,11 +8,15 @@ Usage:
|
|
|
8
8
|
Subcommands:
|
|
9
9
|
create <slug> [--project <id>] Create a mailbox (<slug>@mail.run402.com)
|
|
10
10
|
status [--project <id>] Show mailbox info (ID, address, slug)
|
|
11
|
-
send --
|
|
12
|
-
Send a template email
|
|
11
|
+
send --to <email> [mode flags] Send an email (template or raw HTML)
|
|
13
12
|
list [--project <id>] List sent emails
|
|
14
13
|
get <message_id> [--project <id>] Get a message with replies
|
|
15
14
|
|
|
15
|
+
Send modes:
|
|
16
|
+
Template: --template <name> --var key=value [--var ...]
|
|
17
|
+
Raw HTML: --subject "..." --html "..." [--text "..."]
|
|
18
|
+
Both modes support: --from-name "Display Name" --project <id>
|
|
19
|
+
|
|
16
20
|
Templates:
|
|
17
21
|
project_invite — requires --var project_name=... --var invite_url=...
|
|
18
22
|
magic_link — requires --var project_name=... --var link_url=... --var expires_in=...
|
|
@@ -22,6 +26,8 @@ Examples:
|
|
|
22
26
|
run402 email create my-app
|
|
23
27
|
run402 email send --template project_invite --to user@example.com \\
|
|
24
28
|
--var project_name="My App" --var invite_url="https://example.com/invite/abc"
|
|
29
|
+
run402 email send --to user@example.com --subject "Welcome!" \\
|
|
30
|
+
--html "<h1>Hello</h1><p>Welcome aboard.</p>" --from-name "My App"
|
|
25
31
|
run402 email send --template notification --to admin@example.com \\
|
|
26
32
|
--var project_name="My App" --var message="Deploy complete"
|
|
27
33
|
run402 email list
|
|
@@ -31,7 +37,7 @@ Notes:
|
|
|
31
37
|
- One mailbox per project
|
|
32
38
|
- Single recipient per send (no CC/BCC)
|
|
33
39
|
- Slug: 3-63 chars, lowercase alphanumeric + hyphens, no consecutive hyphens
|
|
34
|
-
- Rate limits vary by tier (prototype: 10/day, hobby: 50/day, team:
|
|
40
|
+
- Rate limits vary by tier (prototype: 10/day, hobby: 50/day, team: 500/day)
|
|
35
41
|
- --project defaults to the active project
|
|
36
42
|
`;
|
|
37
43
|
|
|
@@ -147,25 +153,43 @@ async function create(args) {
|
|
|
147
153
|
async function send(args) {
|
|
148
154
|
const template = parseFlag(args, "--template");
|
|
149
155
|
const to = parseFlag(args, "--to");
|
|
156
|
+
const subject = parseFlag(args, "--subject");
|
|
157
|
+
const html = parseFlag(args, "--html");
|
|
158
|
+
const text = parseFlag(args, "--text");
|
|
159
|
+
const fromName = parseFlag(args, "--from-name");
|
|
150
160
|
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
151
161
|
const p = findProject(projectId);
|
|
152
162
|
const variables = parseVars(args);
|
|
153
163
|
|
|
154
|
-
if (!template) {
|
|
155
|
-
console.error(JSON.stringify({ status: "error", message: "Missing --template. Options: project_invite, magic_link, notification" }));
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
164
|
if (!to) {
|
|
159
165
|
console.error(JSON.stringify({ status: "error", message: "Missing --to <email>" }));
|
|
160
166
|
process.exit(1);
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
const isRaw = !!(subject || html);
|
|
170
|
+
const isTemplate = !!template;
|
|
171
|
+
if (!isRaw && !isTemplate) {
|
|
172
|
+
console.error(JSON.stringify({ status: "error", message: "Provide --template (template mode) or --subject + --html (raw HTML mode)" }));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
163
176
|
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
164
177
|
|
|
178
|
+
const body = { to };
|
|
179
|
+
if (isTemplate) {
|
|
180
|
+
body.template = template;
|
|
181
|
+
body.variables = variables;
|
|
182
|
+
} else {
|
|
183
|
+
body.subject = subject;
|
|
184
|
+
body.html = html;
|
|
185
|
+
if (text) body.text = text;
|
|
186
|
+
}
|
|
187
|
+
if (fromName) body.from_name = fromName;
|
|
188
|
+
|
|
165
189
|
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
|
|
166
190
|
method: "POST",
|
|
167
191
|
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
168
|
-
body: JSON.stringify(
|
|
192
|
+
body: JSON.stringify(body),
|
|
169
193
|
});
|
|
170
194
|
const data = await res.json();
|
|
171
195
|
if (!res.ok) {
|
|
@@ -173,7 +197,7 @@ async function send(args) {
|
|
|
173
197
|
process.exit(1);
|
|
174
198
|
}
|
|
175
199
|
|
|
176
|
-
console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, template: data.template }));
|
|
200
|
+
console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, template: data.template || null, subject: data.subject || null }));
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
async function list(args) {
|
package/lib/functions.mjs
CHANGED
|
@@ -12,7 +12,10 @@ Subcommands:
|
|
|
12
12
|
Deploy a function to a project
|
|
13
13
|
invoke <id> <name> [--method <M>] [--body <json>]
|
|
14
14
|
Invoke a deployed function
|
|
15
|
-
logs <id> <name> [--tail <n>]
|
|
15
|
+
logs <id> <name> [--tail <n>] [--since <ts>] [--follow]
|
|
16
|
+
Get function logs
|
|
17
|
+
update <id> <name> [--schedule <cron>] [--schedule-remove] [--timeout <s>] [--memory <mb>]
|
|
18
|
+
Update function schedule or config without re-deploying
|
|
16
19
|
list <id> List all functions for a project
|
|
17
20
|
delete <id> <name> Delete a function
|
|
18
21
|
|
|
@@ -22,6 +25,11 @@ Examples:
|
|
|
22
25
|
run402 functions deploy abc123 send-reminders --file remind.ts --schedule '' # remove schedule
|
|
23
26
|
run402 functions invoke abc123 stripe-webhook --body '{"event":"test"}'
|
|
24
27
|
run402 functions logs abc123 stripe-webhook --tail 100
|
|
28
|
+
run402 functions logs abc123 stripe-webhook --since 2026-03-29T14:00:00Z
|
|
29
|
+
run402 functions logs abc123 stripe-webhook --follow
|
|
30
|
+
run402 functions update abc123 send-reminders --schedule '0 */4 * * *'
|
|
31
|
+
run402 functions update abc123 send-reminders --schedule-remove
|
|
32
|
+
run402 functions update abc123 my-func --timeout 15 --memory 256
|
|
25
33
|
run402 functions list abc123
|
|
26
34
|
run402 functions delete abc123 stripe-webhook
|
|
27
35
|
|
|
@@ -84,11 +92,94 @@ async function invoke(projectId, name, args) {
|
|
|
84
92
|
async function logs(projectId, name, args) {
|
|
85
93
|
const p = findProject(projectId);
|
|
86
94
|
let tail = 50;
|
|
95
|
+
let since = undefined;
|
|
96
|
+
let follow = false;
|
|
87
97
|
for (let i = 0; i < args.length; i++) {
|
|
88
98
|
if (args[i] === "--tail" && args[i + 1]) tail = parseInt(args[++i]);
|
|
99
|
+
if (args[i] === "--since" && args[i + 1]) since = args[++i];
|
|
100
|
+
if (args[i] === "--follow") follow = true;
|
|
89
101
|
}
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
|
|
103
|
+
// Parse since: accept ISO string or epoch ms
|
|
104
|
+
let sinceMs = undefined;
|
|
105
|
+
if (since !== undefined) {
|
|
106
|
+
const parsed = Number(since);
|
|
107
|
+
sinceMs = Number.isNaN(parsed) ? new Date(since).getTime() : parsed;
|
|
108
|
+
if (Number.isNaN(sinceMs)) { console.error(JSON.stringify({ status: "error", message: `Invalid --since value: ${since}` })); process.exit(1); }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fetchLogs = async () => {
|
|
112
|
+
let url = `${API}/projects/v1/admin/${projectId}/functions/${encodeURIComponent(name)}/logs?tail=${tail}`;
|
|
113
|
+
if (sinceMs !== undefined) url += `&since=${sinceMs}`;
|
|
114
|
+
const res = await fetch(url, { headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
117
|
+
return data.logs || [];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (!follow) {
|
|
121
|
+
const entries = await fetchLogs();
|
|
122
|
+
console.log(JSON.stringify({ logs: entries }, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Follow mode: poll every 3s, print new entries
|
|
127
|
+
let running = true;
|
|
128
|
+
process.on("SIGINT", () => { running = false; });
|
|
129
|
+
|
|
130
|
+
// Initial fetch
|
|
131
|
+
const initial = await fetchLogs();
|
|
132
|
+
for (const entry of initial) {
|
|
133
|
+
console.log(`[${entry.timestamp}] ${entry.message}`);
|
|
134
|
+
}
|
|
135
|
+
if (initial.length > 0) {
|
|
136
|
+
sinceMs = new Date(initial[initial.length - 1].timestamp).getTime() + 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
while (running) {
|
|
140
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
141
|
+
if (!running) break;
|
|
142
|
+
const entries = await fetchLogs();
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
console.log(`[${entry.timestamp}] ${entry.message}`);
|
|
145
|
+
}
|
|
146
|
+
if (entries.length > 0) {
|
|
147
|
+
sinceMs = new Date(entries[entries.length - 1].timestamp).getTime() + 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function update(projectId, name, args) {
|
|
153
|
+
const p = findProject(projectId);
|
|
154
|
+
let schedule = undefined;
|
|
155
|
+
let scheduleRemove = false;
|
|
156
|
+
let timeout = undefined;
|
|
157
|
+
let memory = undefined;
|
|
158
|
+
for (let i = 0; i < args.length; i++) {
|
|
159
|
+
if (args[i] === "--schedule" && i + 1 < args.length) schedule = args[++i];
|
|
160
|
+
if (args[i] === "--schedule-remove") scheduleRemove = true;
|
|
161
|
+
if (args[i] === "--timeout" && args[i + 1]) timeout = parseInt(args[++i]);
|
|
162
|
+
if (args[i] === "--memory" && args[i + 1]) memory = parseInt(args[++i]);
|
|
163
|
+
}
|
|
164
|
+
const body = {};
|
|
165
|
+
if (scheduleRemove || schedule === "") {
|
|
166
|
+
body.schedule = null;
|
|
167
|
+
} else if (schedule !== undefined) {
|
|
168
|
+
body.schedule = schedule;
|
|
169
|
+
}
|
|
170
|
+
if (timeout !== undefined || memory !== undefined) {
|
|
171
|
+
body.config = {};
|
|
172
|
+
if (timeout !== undefined) body.config.timeout = timeout;
|
|
173
|
+
if (memory !== undefined) body.config.memory = memory;
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(body).length === 0) {
|
|
176
|
+
console.error(JSON.stringify({ status: "error", message: "Provide at least one of: --schedule, --schedule-remove, --timeout, --memory" }));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/functions/${encodeURIComponent(name)}`, {
|
|
180
|
+
method: "PATCH",
|
|
181
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify(body),
|
|
92
183
|
});
|
|
93
184
|
const data = await res.json();
|
|
94
185
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
@@ -125,6 +216,7 @@ export async function run(sub, args) {
|
|
|
125
216
|
case "deploy": await deploy(args[0], args[1], args.slice(2)); break;
|
|
126
217
|
case "invoke": await invoke(args[0], args[1], args.slice(2)); break;
|
|
127
218
|
case "logs": await logs(args[0], args[1], args.slice(2)); break;
|
|
219
|
+
case "update": await update(args[0], args[1], args.slice(2)); break;
|
|
128
220
|
case "list": await list(args[0]); break;
|
|
129
221
|
case "delete": await deleteFunction(args[0], args[1]); break;
|
|
130
222
|
default:
|
package/package.json
CHANGED