@upend/cli 0.1.4 → 0.1.5

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 CHANGED
@@ -193,6 +193,40 @@ Neon needs to reach your JWKS URL to validate JWTs for the Data API. After your
193
193
  bunx upend setup:jwks
194
194
  ```
195
195
 
196
+ ### Operations
197
+
198
+ ```bash
199
+ # check service health, disk, memory, cron jobs
200
+ bunx upend status
201
+
202
+ # tail logs (all services, or pick one)
203
+ bunx upend logs
204
+ bunx upend logs api
205
+ bunx upend logs claude
206
+ bunx upend logs -f # follow in realtime
207
+
208
+ # SSH into the remote instance
209
+ bunx upend ssh # interactive shell, cd'd to project
210
+ bunx upend ssh "bun -v" # run a command
211
+ ```
212
+
213
+ ### Workflows
214
+
215
+ Workflows are TypeScript files in `workflows/` that run on a cron schedule or manually:
216
+
217
+ ```bash
218
+ # list workflows and their schedules
219
+ bunx upend workflows
220
+
221
+ # run one manually
222
+ bunx upend workflows run cleanup-sessions
223
+
224
+ # install cron schedules (also happens on deploy)
225
+ bunx upend workflows install
226
+ ```
227
+
228
+ Workflows are also visible in the dashboard with a manual trigger button.
229
+
196
230
  ## CLI Commands
197
231
 
198
232
  | Command | What |
@@ -201,6 +235,11 @@ bunx upend setup:jwks
201
235
  | `upend dev` | Start gateway + claude + caddy locally |
202
236
  | `upend migrate` | Run SQL migrations from `migrations/` |
203
237
  | `upend deploy` | rsync to remote, install, migrate, restart |
238
+ | `upend status` | Check remote service health |
239
+ | `upend logs [service]` | Tail remote logs (`-f` to follow) |
240
+ | `upend ssh [cmd]` | SSH into remote instance |
241
+ | `upend workflows` | List, run, or install workflow cron schedules |
242
+ | `upend env:set <K> <V>` | Set an env var (decrypts, sets, re-encrypts) |
204
243
  | `upend infra:aws` | Provision an EC2 instance |
205
244
 
206
245
  ## Config
package/bin/cli.ts CHANGED
@@ -22,6 +22,9 @@ const commands: Record<string, () => Promise<void>> = {
22
22
  infra: () => import("../src/commands/infra").then((m) => m.default(args.slice(1))),
23
23
  env: () => import("../src/commands/env").then((m) => m.default(args.slice(1))),
24
24
  workflows: () => import("../src/commands/workflows").then((m) => m.default(args.slice(1))),
25
+ logs: () => import("../src/commands/logs").then((m) => m.default(args.slice(1))),
26
+ status: () => import("../src/commands/status").then((m) => m.default(args.slice(1))),
27
+ ssh: () => import("../src/commands/ssh").then((m) => m.default(args.slice(1))),
25
28
  };
26
29
 
27
30
  if (!command || command === "--help" || command === "-h") {
@@ -37,6 +40,10 @@ if (!command || command === "--help" || command === "-h") {
37
40
  upend workflows list workflows
38
41
  upend workflows run <n> run a workflow manually
39
42
  upend workflows install install cron schedules
43
+ upend logs [service] tail remote logs (api|claude|caddy|all)
44
+ upend logs -f follow logs in realtime
45
+ upend status check remote service health
46
+ upend ssh [cmd] SSH into remote (or run a command)
40
47
  upend infra:aws provision AWS infrastructure
41
48
 
42
49
  options:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upend/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Anti-SaaS stack. Deploy live apps with Claude, Postgres, and rsync.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,43 @@
1
+ import { log } from "../lib/log";
2
+ import { exec } from "../lib/exec";
3
+
4
+ export default async function logs(args: string[]) {
5
+ const host = process.env.DEPLOY_HOST;
6
+ const sshKey = process.env.DEPLOY_SSH_KEY || `${process.env.HOME}/.ssh/upend.pem`;
7
+
8
+ if (!host) {
9
+ log.error("DEPLOY_HOST not set. Add it to .env");
10
+ process.exit(1);
11
+ }
12
+
13
+ const service = args[0]; // api, claude, caddy, workflow-<name>, or blank for all
14
+ let logFiles: string;
15
+
16
+ if (service === "api") {
17
+ logFiles = "/tmp/upend-api.log";
18
+ } else if (service === "claude") {
19
+ logFiles = "/tmp/upend-claude.log";
20
+ } else if (service === "caddy") {
21
+ logFiles = "/tmp/upend-caddy.log";
22
+ } else if (service?.startsWith("workflow-")) {
23
+ logFiles = `/tmp/upend-workflow-${service.replace("workflow-", "")}.log`;
24
+ } else {
25
+ logFiles = "/tmp/upend-api.log /tmp/upend-claude.log /tmp/upend-caddy.log";
26
+ }
27
+
28
+ const lines = args.includes("-n") ? args[args.indexOf("-n") + 1] || "50" : "50";
29
+ const follow = args.includes("-f") || args.includes("--follow");
30
+
31
+ const tailCmd = follow
32
+ ? `tail -f ${logFiles}`
33
+ : `tail -n ${lines} ${logFiles}`;
34
+
35
+ log.dim(`ssh ${host} → ${tailCmd}`);
36
+
37
+ // use spawn for streaming output (especially -f)
38
+ const proc = Bun.spawn(["ssh", "-i", sshKey, host, tailCmd], {
39
+ stdout: "inherit",
40
+ stderr: "inherit",
41
+ });
42
+ await proc.exited;
43
+ }
@@ -0,0 +1,32 @@
1
+ import { log } from "../lib/log";
2
+
3
+ export default async function ssh(args: string[]) {
4
+ const host = process.env.DEPLOY_HOST;
5
+ const sshKey = process.env.DEPLOY_SSH_KEY || `${process.env.HOME}/.ssh/upend.pem`;
6
+ const appDir = process.env.DEPLOY_DIR || "/opt/upend";
7
+
8
+ if (!host) {
9
+ log.error("DEPLOY_HOST not set. Add it to .env");
10
+ process.exit(1);
11
+ }
12
+
13
+ // if args provided, run as remote command
14
+ if (args.length > 0) {
15
+ const cmd = args.join(" ");
16
+ log.dim(`ssh ${host} → ${cmd}`);
17
+ const proc = Bun.spawn(["ssh", "-i", sshKey, host, `cd ${appDir} && ${cmd}`], {
18
+ stdout: "inherit",
19
+ stderr: "inherit",
20
+ });
21
+ process.exit(await proc.exited);
22
+ }
23
+
24
+ // otherwise, interactive shell
25
+ log.dim(`ssh ${host} (cd ${appDir})`);
26
+ const proc = Bun.spawn(["ssh", "-i", sshKey, "-t", host, `cd ${appDir} && exec bash`], {
27
+ stdout: "inherit",
28
+ stderr: "inherit",
29
+ stdin: "inherit",
30
+ });
31
+ process.exit(await proc.exited);
32
+ }
@@ -0,0 +1,47 @@
1
+ import { log } from "../lib/log";
2
+ import { exec } from "../lib/exec";
3
+
4
+ export default async function status(args: string[]) {
5
+ const host = process.env.DEPLOY_HOST;
6
+ const sshKey = process.env.DEPLOY_SSH_KEY || `${process.env.HOME}/.ssh/upend.pem`;
7
+
8
+ if (!host) {
9
+ log.error("DEPLOY_HOST not set. Add it to .env");
10
+ process.exit(1);
11
+ }
12
+
13
+ const appDir = process.env.DEPLOY_DIR || "/opt/upend";
14
+
15
+ log.header(`${host}`);
16
+
17
+ // check services
18
+ const { stdout } = await exec(["ssh", "-i", sshKey, host, `bash -c '
19
+ echo "=== services ==="
20
+ pgrep -af "bun services/api" > /dev/null && echo "api: running" || echo "api: stopped"
21
+ pgrep -af "bun services/claude" > /dev/null && echo "claude: running" || echo "claude: stopped"
22
+ pgrep -af "caddy" > /dev/null && echo "caddy: running" || echo "caddy: stopped"
23
+
24
+ echo ""
25
+ echo "=== health ==="
26
+ curl -s -o /dev/null -w "api: %{http_code}" http://localhost:3001/ 2>/dev/null || echo "api: unreachable"
27
+ echo ""
28
+ curl -s -o /dev/null -w "caddy: %{http_code}" http://localhost:80/ 2>/dev/null || echo "caddy: unreachable"
29
+ echo ""
30
+
31
+ echo ""
32
+ echo "=== system ==="
33
+ uptime
34
+ df -h / | tail -1 | awk "{print \"disk: \" \\$3 \" used / \" \\$2 \" (\" \\$5 \")\"}"
35
+ free -h 2>/dev/null | awk "/Mem:/{print \"memory: \" \\$3 \" used / \" \\$2}" || echo "memory: n/a"
36
+
37
+ echo ""
38
+ echo "=== workflows (crontab) ==="
39
+ crontab -l 2>/dev/null | grep "upend-workflow" || echo "none installed"
40
+
41
+ echo ""
42
+ echo "=== last deploy ==="
43
+ cd ${appDir} && git log --oneline -1 2>/dev/null || echo "no git history"
44
+ '`]);
45
+
46
+ console.log(stdout);
47
+ }
@@ -27,7 +27,7 @@ export const requireAuth = createMiddleware<{
27
27
  const user = {
28
28
  sub: payload.sub as string,
29
29
  email: payload.email as string,
30
- role: (payload.role as string) || "user",
30
+ role: (payload.app_role as string) || (payload.role as string) || "user",
31
31
  };
32
32
  console.log(`[auth] ${user.email} → ${method} ${path}`);
33
33
  c.set("user", user);