@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 +39 -0
- package/bin/cli.ts +7 -0
- package/package.json +1 -1
- package/src/commands/logs.ts +43 -0
- package/src/commands/ssh.ts +32 -0
- package/src/commands/status.ts +47 -0
- package/src/lib/middleware.ts +1 -1
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
|
@@ -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
|
+
}
|
package/src/lib/middleware.ts
CHANGED
|
@@ -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);
|