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 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 template-based emails from your project
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 --template <name> --to <email> [--var key=value ...] [--project <id>]
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: 200/day)
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({ template, to, variables }),
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>] Get function logs
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
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/functions/${encodeURIComponent(name)}/logs?tail=${tail}`, {
91
- headers: { "Authorization": `Bearer ${p.service_key}` },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.20.1",
3
+ "version": "1.22.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {