From 06591b79182a4fa7113a39aec8805bdf8bdc8ea5 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Mon, 30 Mar 2026 15:23:32 +0300 Subject: [PATCH 01/18] feat: update cli to work with up to date developer api --- .gitignore | 1 + CLAUDE.md | 330 ++++++++++++++++++ README.md | 239 +++++++++---- cmd/build.ts | 64 ---- package-lock.json | 66 ++++ package.json | 30 ++ skill.md | 329 +++++++++++++++++ src/auth.ts | 50 +++ src/cli.ts | 24 ++ src/client.ts | 29 ++ src/commands/auth/index.ts | 48 +++ src/commands/auth/login.ts | 60 ---- src/commands/auth/logout.ts | 13 - src/commands/auth/mod.ts | 17 - src/commands/auth/whoami.ts | 16 - src/commands/qstash/disable-prodpack.ts | 25 ++ src/commands/qstash/enable-prodpack.ts | 25 ++ src/commands/qstash/get.ts | 26 ++ src/commands/qstash/index.ts | 26 ++ src/commands/qstash/ipv4.ts | 27 ++ src/commands/qstash/list.ts | 40 +++ src/commands/qstash/move-to-team.ts | 39 +++ src/commands/qstash/rotate-token.ts | 32 ++ src/commands/qstash/set-plan.ts | 29 ++ src/commands/qstash/stats.ts | 35 ++ src/commands/qstash/update-budget.ts | 32 ++ src/commands/redis/backup/create.ts | 36 ++ src/commands/redis/backup/delete.ts | 53 +++ src/commands/redis/backup/disable-daily.ts | 32 ++ src/commands/redis/backup/enable-daily.ts | 32 ++ src/commands/redis/backup/index.ts | 18 + src/commands/redis/backup/list.ts | 48 +++ src/commands/redis/backup/restore.ts | 42 +++ src/commands/redis/change-plan.ts | 38 ++ src/commands/redis/create.ts | 164 +++------ src/commands/redis/delete.ts | 84 +++-- src/commands/redis/disable-autoupgrade.ts | 32 ++ src/commands/redis/disable-eviction.ts | 32 ++ src/commands/redis/enable-autoupgrade.ts | 32 ++ src/commands/redis/enable-eviction.ts | 32 ++ src/commands/redis/enable-tls.ts | 32 ++ .../redis/enable_multizone_replication.ts | 49 --- src/commands/redis/get.ts | 80 ++--- src/commands/redis/index.ts | 40 +++ src/commands/redis/list.ts | 90 ++--- src/commands/redis/mod.ts | 28 -- src/commands/redis/move-to-team.ts | 37 ++ src/commands/redis/move_to_team.ts | 65 ---- src/commands/redis/rename.ts | 91 ++--- src/commands/redis/reset-password.ts | 39 +++ src/commands/redis/reset_password.ts | 55 --- src/commands/redis/stats.ts | 67 ++-- src/commands/redis/types.ts | 9 - src/commands/redis/update-budget.ts | 36 ++ src/commands/redis/update-regions.ts | 44 +++ src/commands/search/create.ts | 49 +++ src/commands/search/delete.ts | 32 ++ src/commands/search/get.ts | 26 ++ src/commands/search/index.ts | 22 ++ src/commands/search/list.ts | 30 ++ src/commands/search/rename.ts | 34 ++ src/commands/search/reset-password.ts | 32 ++ src/commands/search/stats.ts | 51 +++ src/commands/search/transfer.ts | 31 ++ src/commands/team/add-member.ts | 56 +++ src/commands/team/add_member.ts | 93 ----- src/commands/team/create.ts | 85 +++-- src/commands/team/delete.ts | 82 ++--- src/commands/team/index.ts | 18 + src/commands/team/list.ts | 70 ++-- src/commands/team/list_members.ts | 55 --- src/commands/team/members.ts | 40 +++ src/commands/team/mod.ts | 21 -- src/commands/team/remove-member.ts | 58 +++ src/commands/team/remove_member.ts | 58 --- src/commands/vector/create.ts | 83 +++++ src/commands/vector/delete.ts | 32 ++ src/commands/vector/get.ts | 26 ++ src/commands/vector/index.ts | 24 ++ src/commands/vector/list.ts | 37 ++ src/commands/vector/rename.ts | 34 ++ src/commands/vector/reset-password.ts | 32 ++ src/commands/vector/set-plan.ts | 29 ++ src/commands/vector/stats.ts | 55 +++ src/commands/vector/transfer.ts | 31 ++ src/config.ts | 26 -- src/deps.ts | 3 - src/mod.ts | 48 --- src/output.ts | 37 ++ src/types.ts | 171 +++++++++ src/util/auth.ts | 32 -- src/util/command.ts | 11 - src/util/http.ts | 49 --- src/version.ts | 2 - tsconfig.json | 14 + 95 files changed, 3511 insertions(+), 1297 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 cmd/build.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 skill.md create mode 100644 src/auth.ts create mode 100644 src/cli.ts create mode 100644 src/client.ts create mode 100644 src/commands/auth/index.ts delete mode 100644 src/commands/auth/login.ts delete mode 100644 src/commands/auth/logout.ts delete mode 100644 src/commands/auth/mod.ts delete mode 100644 src/commands/auth/whoami.ts create mode 100644 src/commands/qstash/disable-prodpack.ts create mode 100644 src/commands/qstash/enable-prodpack.ts create mode 100644 src/commands/qstash/get.ts create mode 100644 src/commands/qstash/index.ts create mode 100644 src/commands/qstash/ipv4.ts create mode 100644 src/commands/qstash/list.ts create mode 100644 src/commands/qstash/move-to-team.ts create mode 100644 src/commands/qstash/rotate-token.ts create mode 100644 src/commands/qstash/set-plan.ts create mode 100644 src/commands/qstash/stats.ts create mode 100644 src/commands/qstash/update-budget.ts create mode 100644 src/commands/redis/backup/create.ts create mode 100644 src/commands/redis/backup/delete.ts create mode 100644 src/commands/redis/backup/disable-daily.ts create mode 100644 src/commands/redis/backup/enable-daily.ts create mode 100644 src/commands/redis/backup/index.ts create mode 100644 src/commands/redis/backup/list.ts create mode 100644 src/commands/redis/backup/restore.ts create mode 100644 src/commands/redis/change-plan.ts create mode 100644 src/commands/redis/disable-autoupgrade.ts create mode 100644 src/commands/redis/disable-eviction.ts create mode 100644 src/commands/redis/enable-autoupgrade.ts create mode 100644 src/commands/redis/enable-eviction.ts create mode 100644 src/commands/redis/enable-tls.ts delete mode 100644 src/commands/redis/enable_multizone_replication.ts create mode 100644 src/commands/redis/index.ts delete mode 100644 src/commands/redis/mod.ts create mode 100644 src/commands/redis/move-to-team.ts delete mode 100644 src/commands/redis/move_to_team.ts create mode 100644 src/commands/redis/reset-password.ts delete mode 100644 src/commands/redis/reset_password.ts delete mode 100644 src/commands/redis/types.ts create mode 100644 src/commands/redis/update-budget.ts create mode 100644 src/commands/redis/update-regions.ts create mode 100644 src/commands/search/create.ts create mode 100644 src/commands/search/delete.ts create mode 100644 src/commands/search/get.ts create mode 100644 src/commands/search/index.ts create mode 100644 src/commands/search/list.ts create mode 100644 src/commands/search/rename.ts create mode 100644 src/commands/search/reset-password.ts create mode 100644 src/commands/search/stats.ts create mode 100644 src/commands/search/transfer.ts create mode 100644 src/commands/team/add-member.ts delete mode 100644 src/commands/team/add_member.ts create mode 100644 src/commands/team/index.ts delete mode 100644 src/commands/team/list_members.ts create mode 100644 src/commands/team/members.ts delete mode 100644 src/commands/team/mod.ts create mode 100644 src/commands/team/remove-member.ts delete mode 100644 src/commands/team/remove_member.ts create mode 100644 src/commands/vector/create.ts create mode 100644 src/commands/vector/delete.ts create mode 100644 src/commands/vector/get.ts create mode 100644 src/commands/vector/index.ts create mode 100644 src/commands/vector/list.ts create mode 100644 src/commands/vector/rename.ts create mode 100644 src/commands/vector/reset-password.ts create mode 100644 src/commands/vector/set-plan.ts create mode 100644 src/commands/vector/stats.ts create mode 100644 src/commands/vector/transfer.ts delete mode 100644 src/config.ts delete mode 100644 src/deps.ts delete mode 100644 src/mod.ts create mode 100644 src/output.ts create mode 100644 src/types.ts delete mode 100644 src/util/auth.ts delete mode 100644 src/util/command.ts delete mode 100644 src/util/http.ts delete mode 100644 src/version.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 5bfcd1f..0672e36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env dist +node_modules diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..54ad1a7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,330 @@ + +# Upstash CLI — Agent Skill + +The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and supports `--json` for structured output. + +## Installation + +```bash +# From the repo root +npm install +npm run build +npm link +``` + +## Authentication + +Set environment variables (recommended for agents): + +```bash +export UPSTASH_EMAIL=you@example.com +export UPSTASH_API_KEY=your_api_key +``` + +Or save to `~/.upstash.json`: + +```bash +upstash auth login --email you@example.com --api-key your_api_key +``` + +## Global Flags + +Every command accepts these flags: + +| Flag | Description | +|------|-------------| +| `--email ` | Upstash email (overrides env/config) | +| `--api-key ` | Upstash API key (overrides env/config) | +| `--json` | Output structured JSON instead of human-readable text | + +## Output Formats + +### Success with data (`--json`) +```json +{ "database_id": "...", "database_name": "mydb", "state": "active", ... } +``` + +### Boolean operation success (`--json`) +```json +{ "success": true, "database_id": "..." } +``` + +### Delete success (`--json`) +```json +{ "deleted": true, "database_id": "..." } +``` + +### Error (`--json`, exits with code 1) +```json +{ "error": "detailed error message" } +``` + +--- + +## Auth Commands + +```bash +upstash auth login --email --api-key --json # Save credentials +upstash auth logout --json # Clear credentials +upstash auth whoami --json # Show current email +``` + +--- + +## Redis Commands + +### Core CRUD + +```bash +upstash redis list --json +upstash redis get --json +upstash redis get --hide-credentials --json # Omit password from output +upstash redis create --name --region --json +upstash redis create --name --region --read-regions --json +upstash redis delete --dry-run --json # Preview before deleting +upstash redis delete --json +upstash redis rename --name --json +upstash redis reset-password --json +upstash redis stats --json +``` + +### Available Redis Regions + +**AWS:** `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1` + +**GCP:** `us-central1`, `us-east4`, `europe-west1`, `asia-northeast1` + +### Configuration + +```bash +upstash redis enable-tls --json +upstash redis enable-eviction --json +upstash redis disable-eviction --json +upstash redis enable-autoupgrade --json +upstash redis disable-autoupgrade --json +upstash redis change-plan --plan --json # Plans: free, payg, pro, paid +upstash redis update-budget --budget --json # Monthly budget in cents +upstash redis update-regions --read-regions --json +upstash redis move-to-team --team-id --json +``` + +### Backups + +```bash +upstash redis backup list --json +upstash redis backup create --name --json +upstash redis backup delete --dry-run --json +upstash redis backup delete --json +upstash redis backup restore --backup-id --json +upstash redis backup enable-daily --json +upstash redis backup disable-daily --json +``` + +### Redis Database Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `database_id` | string | Unique identifier (UUID) | +| `database_name` | string | Display name | +| `endpoint` | string | Redis connection hostname | +| `port` | number | Redis port | +| `password` | string | Redis password (omitted with `--hide-credentials`) | +| `state` | string | `active`, `suspended`, or `passive` | +| `tls` | boolean | TLS enabled | +| `type` | string | `free`, `payg`, `pro`, or `paid` | +| `primary_region` | string | Primary region | +| `read_regions` | string[] | Read replica regions | +| `eviction` | boolean | Key eviction enabled | +| `auto_upgrade` | boolean | Auto version upgrade enabled | +| `daily_backup_enabled` | boolean | Daily backups enabled | +| `budget` | number | Monthly spend cap in cents | +| `creation_time` | number | Unix timestamp | + +--- + +## Team Commands + +```bash +upstash team list --json +upstash team create --name --json +upstash team create --name --copy-cc --json # Copy credit card to team +upstash team delete --dry-run --json +upstash team delete --json +upstash team members --json # List team members +upstash team add-member --team-id --member-email --role --json +upstash team remove-member --team-id --member-email --dry-run --json +upstash team remove-member --team-id --member-email --json +``` + +Member roles: `admin`, `dev`, `finance` + +### Team Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `team_id` | string | Unique team identifier | +| `team_name` | string | Team display name | +| `copy_cc` | boolean | Credit card copied to team | + +### TeamMember Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `team_id` | string | Team identifier | +| `member_email` | string | Member email address | +| `member_role` | string | `owner`, `admin`, `dev`, or `finance` | + +--- + +## Vector Commands + +```bash +upstash vector list --json +upstash vector get --json +upstash vector create --name --region --similarity-function --dimension-count --json +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg --json +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 --json +upstash vector delete --dry-run --json +upstash vector delete --json +upstash vector rename --name --json +upstash vector reset-password --json +upstash vector set-plan --plan --json # Plans: free, payg, fixed +upstash vector transfer --target-account --json +upstash vector stats --json # Aggregate stats across all indexes +upstash vector index-stats --json +upstash vector index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +``` + +### Available Vector Regions + +`eu-west-1`, `us-east-1`, `us-central1` + +### Similarity Functions + +`COSINE`, `EUCLIDEAN`, `DOT_PRODUCT` + +### Index Types + +`DENSE`, `SPARSE`, `HYBRID` + +### Embedding Models + +`BGE_SMALL_EN_V1_5`, `BGE_BASE_EN_V1_5`, `BGE_LARGE_EN_V1_5`, `BGE_M3` + +### Sparse Embedding Models + +`BM25`, `BGE_M3` + +### VectorIndex Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique index identifier | +| `name` | string | Index name | +| `region` | string | Deployment region | +| `similarity_function` | string | Distance metric | +| `dimension_count` | number | Dimensions per vector | +| `index_type` | string | `DENSE`, `SPARSE`, or `HYBRID` | +| `embedding_model` | string | Dense embedding model (if set) | +| `sparse_embedding_model` | string | Sparse embedding model (if set) | +| `endpoint` | string | REST endpoint hostname | +| `token` | string | Read-write auth token | +| `read_only_token` | string | Read-only auth token | +| `type` | string | `free`, `payg`, or `fixed` | +| `max_vector_count` | number | Vector capacity | +| `creation_time` | number | Unix timestamp | + +--- + +## Search Commands + +```bash +upstash search list --json +upstash search get --json +upstash search create --name --region --type --json +upstash search delete --dry-run --json +upstash search delete --json +upstash search rename --name --json +upstash search reset-password --json +upstash search transfer --target-account --json +upstash search stats --json # Aggregate stats across all indexes +upstash search index-stats --json +upstash search index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +``` + +### Available Search Regions + +`eu-west-1`, `us-central1` + +### Search Plans + +`free`, `payg`, `fixed` + +### SearchIndex Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique index identifier | +| `name` | string | Index name | +| `region` | string | Deployment region | +| `type` | string | `free`, `payg`, or `fixed` | +| `endpoint` | string | REST endpoint hostname | +| `token` | string | Read-write auth token | +| `read_only_token` | string | Read-only auth token | +| `input_enrichment_enabled` | boolean | Input enrichment enabled | +| `creation_time` | number | Unix timestamp | + +--- + +## QStash Commands + +```bash +upstash qstash list --json # All instances; map `region` → `id` for other commands +upstash qstash get --json +upstash qstash rotate-token --json +upstash qstash set-plan --plan --json # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m +upstash qstash stats --json +upstash qstash stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash qstash ipv4 --json # CIDR blocks for firewall allowlisting +upstash qstash move-to-team --qstash-id --target-team-id --json +upstash qstash update-budget --budget --json # 0 = no limit +upstash qstash enable-prodpack --json +upstash qstash disable-prodpack --json +``` + +### QStashUser Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | QStash instance identifier | +| `customer_id` | string | Owner email or team ID | +| `token` | string | Auth token for QStash API | +| `state` | string | `active` or `passive` | +| `type` | string | `free` or `paid` | +| `reserved_type` | string | Reserved plan: `paid`, `qstash_fixed_1m`, `qstash_fixed_10m`, `qstash_fixed_100m` | +| `region` | string | `eu-central-1` or `us-east-1` | +| `budget` | number | Monthly spend cap in dollars (0 = no limit) | +| `prod_pack_enabled` | boolean | Production pack active | +| `max_requests_per_day` | number | Daily request soft limit | +| `max_requests_per_second` | number | Rate limit | +| `max_topics` | number | Max topics | +| `max_schedules` | number | Max schedules | +| `max_queues` | number | Max queues | +| `timeout` | number | Request timeout in seconds | +| `creation_time` | number | Unix timestamp | + +--- + +## Tips for Agents + +- Always use `--json` for reliable output parsing. +- Exit code `0` = success, `1` = error. +- Use `--dry-run` before any `delete` or `remove-member` command to confirm the target. +- Use `--hide-credentials` on `redis get` when the password is not needed. +- Pipe `--json` output through `jq` for field extraction: + ```bash + upstash redis list --json | jq '.[].database_id' + upstash vector list --json | jq '.[] | {id, name, region}' + upstash qstash list --json | jq '.[] | {id, region}' + upstash team members --json | jq '.[].member_email' + ``` diff --git a/README.md b/README.md index 608b4c7..4cf37df 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,221 @@ # Upstash CLI -Manage Upstash resources in your terminal or CI. +Manage Upstash services from the terminal or automation via the [Upstash Developer API](https://docs.upstash.com/redis/howto/developerapi). Commands are non-interactive and support `--json` for structured output. ![](./img/banner.svg) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/upstash/cli) [![Downloads/week](https://img.shields.io/npm/dw/lstr.svg)](https://npmjs.org/package/@upstash/cli) -# Installation +**Agent reference:** the same command catalog lives in [`CLAUDE.md`](./CLAUDE.md) and [`skill.md`](./skill.md). -## npm +## Installation -You can install upstash's cli directly from npm +### npm (global) ```bash npm i -g @upstash/cli ``` -It will be added as `upstash` to your system's path. +The binary is `upstash` on your `PATH`. -## Compiled binaries: +### From this repository -`upstash` is also available from the -[releases page](https://github.com/upstash/cli/releases/latest) compiled for -windows, linux and mac (both intel and m1). +```bash +npm install +npm run build +npm link +``` + +Compiled binaries for Windows, Linux, and macOS (Intel and Apple Silicon) are on the [releases page](https://github.com/upstash/cli/releases/latest). -# Usage +## Authentication + +Recommended for CI and agents: ```bash -> upstash +export UPSTASH_EMAIL=you@example.com +export UPSTASH_API_KEY=your_api_key +``` - Usage: upstash - Version: development +Or save credentials to `~/.upstash.json`: - Description: +```bash +upstash auth login --email you@example.com --api-key your_api_key +``` - Official cli for Upstash products +See [how to get an API key](https://docs.upstash.com/redis/howto/developerapi#api-development). - Options: +## Global flags - -h, --help - Show this help. - -V, --version - Show the version number for this program. - -c, --config - Path to .upstash.json file +These flags are accepted on commands that call the API: - Commands: +| Flag | Description | +|------|-------------| +| `--email ` | Upstash email (overrides env / config file) | +| `--api-key ` | Upstash API key (overrides env / config file) | +| `--json` | Print structured JSON instead of human-readable text | - auth - Login and logout - redis - Manage redis database instances - team - Manage your teams and their members +## Top-level commands - Environment variables: +```text +upstash auth # Credentials +upstash redis # Redis databases +upstash team # Teams and members +upstash vector # Vector indexes +upstash search # Search indexes +upstash qstash # QStash instances +``` - UPSTASH_EMAIL - The email you use on upstash - UPSTASH_API_KEY - The api key from upstash +```bash +upstash --help +upstash redis --help ``` -## Authentication +## Output shapes (`--json`) -When running `upstash` for the first time, you should log in using -`upstash auth login`. Provide your email and an api key. -[See here for how to get a key.](https://docs.upstash.com/redis/howto/developerapi#api-development) +- **Resource payload:** e.g. `{ "database_id": "...", "state": "active", ... }` +- **Boolean success:** `{ "success": true, ... }` +- **Delete:** `{ "deleted": true, ... }` +- **Error (exit code 1):** `{ "error": "message" }` -As an alternative to logging in, you can provide `UPSTASH_EMAIL` and -`UPSTASH_API_KEY` as environment variables. +## Auth commands -## Usage +```bash +upstash auth login --email --api-key --json +upstash auth logout --json +upstash auth whoami --json +``` + +## Redis commands -Let's create a new redis database: +### Core +```bash +upstash redis list --json +upstash redis get --json +upstash redis get --hide-credentials --json +upstash redis create --name --region --json +upstash redis create --name --region --read-regions --json +upstash redis delete --dry-run --json +upstash redis delete --json +upstash redis rename --name --json +upstash redis reset-password --json +upstash redis stats --json ``` -> upstash redis create --name=my-db --region=eu-west-1 - Database has been created - - database_id a3e25299-132a-45b9-b026-c73f5a807859 - database_name my-db - database_type Pay as You Go - region eu-west-1 - type paid - port 37090 - creation_time 1652687630 - state active - password 88ae6392a1084d1186a3da37fb5f5a30 - user_email andreas@upstash.com - endpoint eu1-magnetic-lacewing-37090.upstash.io - edge false - multizone false - rest_token AZDiASQgYTNlMjUyOTktMTMyYS00NWI5LWIwMjYtYzczZjVhODA3ODU5ODhhZTYzOTJhMTA4NGQxMTg2YTNkYTM3ZmI1ZjVhMzA= - read_only_rest_token ApDiASQgYTNlMjUyOTktMTMyYS00NWI5LWIwMjYtYzczZjVhODA3ODU5O_InFjRVX1XHsaSjq1wSerFCugZ8t8O1aTfbF6Jhq1I= - - - You can visit your database details page: https://console.upstash.com/redis/a3e25299-132a-45b9-b026-c73f5a807859 - - Connect to your database with redis-cli: redis-cli -u redis://88ae6392a1084d1186a3da37fb5f5a30@eu1-magnetic-lacewing-37090.upstash.io:37090 + +**Regions — AWS:** `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1` +**Regions — GCP:** `us-central1`, `us-east4`, `europe-west1`, `asia-northeast1` + +### Configuration + +```bash +upstash redis enable-tls --json +upstash redis enable-eviction --json +upstash redis disable-eviction --json +upstash redis enable-autoupgrade --json +upstash redis disable-autoupgrade --json +upstash redis change-plan --plan --json # free, payg, pro, paid +upstash redis update-budget --budget --json +upstash redis update-regions --read-regions --json +upstash redis move-to-team --team-id --json ``` -## Output +### Backups -Most commands support the `--json` flag to return the raw api response as json, -which you can parse and automate your system. +```bash +upstash redis backup list --json +upstash redis backup create --name --json +upstash redis backup delete --dry-run --json +upstash redis backup delete --json +upstash redis backup restore --backup-id --json +upstash redis backup enable-daily --json +upstash redis backup disable-daily --json +``` + +## Team commands + +```bash +upstash team list --json +upstash team create --name --json +upstash team create --name --copy-cc --json +upstash team delete --dry-run --json +upstash team delete --json +upstash team members --json +upstash team add-member --team-id --member-email --role --json +upstash team remove-member --team-id --member-email --dry-run --json +upstash team remove-member --team-id --member-email --json +``` + +Member roles: `admin`, `dev`, `finance`. + +## Vector commands + +```bash +upstash vector list --json +upstash vector get --json +upstash vector create --name --region --similarity-function --dimension-count --json +upstash vector delete --dry-run --json +upstash vector delete --json +upstash vector rename --name --json +upstash vector reset-password --json +upstash vector set-plan --plan --json # free, payg, fixed +upstash vector transfer --target-account --json +upstash vector stats --json +upstash vector index-stats --json +upstash vector index-stats --period --json # 1h, 3h, 12h, 1d, 3d, 7d, 30d +``` + +**Regions:** `eu-west-1`, `us-east-1`, `us-central1` +**Similarity:** `COSINE`, `EUCLIDEAN`, `DOT_PRODUCT` +**Index types:** `DENSE`, `SPARSE`, `HYBRID` + +## Search commands + +```bash +upstash search list --json +upstash search get --json +upstash search create --name --region --type --json +upstash search delete --dry-run --json +upstash search delete --json +upstash search rename --name --json +upstash search reset-password --json +upstash search transfer --target-account --json +upstash search stats --json +upstash search index-stats --json +upstash search index-stats --period --json +``` + +**Regions:** `eu-west-1`, `us-central1` +**Plans:** `free`, `payg`, `fixed` + +## QStash commands ```bash -> upstash redis create --name=test2113 --region=us-central1 --json | jq '.endpoint' +upstash qstash list --json # All instances; map region → id for other commands +upstash qstash get --json +upstash qstash rotate-token --json +upstash qstash set-plan --plan --json # paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m +upstash qstash stats --json +upstash qstash stats --period --json # 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash qstash ipv4 --json +upstash qstash move-to-team --qstash-id --target-team-id --json +upstash qstash update-budget --budget --json # 0 = no limit +upstash qstash enable-prodpack --json +upstash qstash disable-prodpack --json +``` + +## Examples - "gusc1-clean-gelding-30208.upstash.io" +```bash +upstash redis list --json | jq '.[].database_id' +upstash vector list --json | jq '.[] | {id, name, region}' +upstash qstash list --json | jq '.[] | {id, region}' +upstash team members --json | jq '.[].member_email' ``` +Use `--dry-run` before `delete` or `remove-member`. Use `--hide-credentials` on `redis get` when you do not need the password. + ## Contributing -If anything feels wrong, you discover a bug or want to request improvements, -please create an issue or talk to us on -[Discord](https://discord.com/invite/w9SenAtbme) +Issues and improvements: open a ticket or talk to us on [Discord](https://discord.com/invite/w9SenAtbme). diff --git a/cmd/build.ts b/cmd/build.ts deleted file mode 100644 index dd81733..0000000 --- a/cmd/build.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { build, emptyDir } from "https://deno.land/x/dnt@0.23.0/mod.ts"; - -const packageManager = "npm"; -const outDir = "./dist"; -const version = Deno.args[0] ?? "development"; - -await emptyDir(outDir); - -Deno.writeTextFileSync( - "./src/version.ts", - `export const VERSION = "${version}"`, -); - -await build({ - packageManager, - entryPoints: [{ kind: "bin", name: "upstash", path: "src/mod.ts" }], - outDir, - shims: { - deno: true, - // undici: true, - custom: [ - { - package: { name: "node-fetch", version: "latest" }, - globalNames: [{ name: "fetch", exportName: "default" }], - }, - ], - }, - - scriptModule: false, - declaration: false, - typeCheck: false, - test: typeof Deno.env.get("TEST") !== "undefined", - package: { - // package.json properties - name: "@upstash/cli", - version, - description: "CLI for Upstash resources.", - repository: { - type: "git", - url: "git+https://github.com/upstash/cli.git", - }, - keywords: ["cli", "redis", "kafka", "serverless", "edge", "upstash"], - contributors: [ - { - name: "Andreas Thomas", - email: "dev@chronark.com", - }, - ], - license: "MIT", - bugs: { - url: "https://github.com/upstash/cli/issues", - }, - homepage: "https://github.com/upstash/cli#readme", - }, -}); -Deno.writeTextFileSync( - "./src/version.ts", - `// This is set during build -export const VERSION = "development";`, -); - -// post build steps -Deno.copyFileSync("LICENSE", `${outDir}/LICENSE`); -Deno.copyFileSync("README.md", `${outDir}/README.md`); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..697c2f0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "@upstash/cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@upstash/cli", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "commander": "^13.0.0" + }, + "bin": { + "upstash": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbfc12b --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "@upstash/cli", + "version": "1.0.0", + "description": "Agent-friendly CLI for Upstash", + "type": "module", + "bin": { + "upstash": "./dist/cli.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch" + }, + "keywords": ["upstash", "cli", "ai-agent", "redis"], + "author": "Upstash", + "license": "MIT", + "dependencies": { + "commander": "^13.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "files": ["dist"] +} diff --git a/skill.md b/skill.md new file mode 100644 index 0000000..1b1328d --- /dev/null +++ b/skill.md @@ -0,0 +1,329 @@ +# Upstash CLI — Agent Skill + +The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and supports `--json` for structured output. + +## Installation + +```bash +# From the repo root +npm install +npm run build +npm link +``` + +## Authentication + +Set environment variables (recommended for agents): + +```bash +export UPSTASH_EMAIL=you@example.com +export UPSTASH_API_KEY=your_api_key +``` + +Or save to `~/.upstash.json`: + +```bash +upstash auth login --email you@example.com --api-key your_api_key +``` + +## Global Flags + +Every command accepts these flags: + +| Flag | Description | +|------|-------------| +| `--email ` | Upstash email (overrides env/config) | +| `--api-key ` | Upstash API key (overrides env/config) | +| `--json` | Output structured JSON instead of human-readable text | + +## Output Formats + +### Success with data (`--json`) +```json +{ "database_id": "...", "database_name": "mydb", "state": "active", ... } +``` + +### Boolean operation success (`--json`) +```json +{ "success": true, "database_id": "..." } +``` + +### Delete success (`--json`) +```json +{ "deleted": true, "database_id": "..." } +``` + +### Error (`--json`, exits with code 1) +```json +{ "error": "detailed error message" } +``` + +--- + +## Auth Commands + +```bash +upstash auth login --email --api-key --json # Save credentials +upstash auth logout --json # Clear credentials +upstash auth whoami --json # Show current email +``` + +--- + +## Redis Commands + +### Core CRUD + +```bash +upstash redis list --json +upstash redis get --json +upstash redis get --hide-credentials --json # Omit password from output +upstash redis create --name --region --json +upstash redis create --name --region --read-regions --json +upstash redis delete --dry-run --json # Preview before deleting +upstash redis delete --json +upstash redis rename --name --json +upstash redis reset-password --json +upstash redis stats --json +``` + +### Available Redis Regions + +**AWS:** `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1` + +**GCP:** `us-central1`, `us-east4`, `europe-west1`, `asia-northeast1` + +### Configuration + +```bash +upstash redis enable-tls --json +upstash redis enable-eviction --json +upstash redis disable-eviction --json +upstash redis enable-autoupgrade --json +upstash redis disable-autoupgrade --json +upstash redis change-plan --plan --json # Plans: free, payg, pro, paid +upstash redis update-budget --budget --json # Monthly budget in cents +upstash redis update-regions --read-regions --json +upstash redis move-to-team --team-id --json +``` + +### Backups + +```bash +upstash redis backup list --json +upstash redis backup create --name --json +upstash redis backup delete --dry-run --json +upstash redis backup delete --json +upstash redis backup restore --backup-id --json +upstash redis backup enable-daily --json +upstash redis backup disable-daily --json +``` + +### Redis Database Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `database_id` | string | Unique identifier (UUID) | +| `database_name` | string | Display name | +| `endpoint` | string | Redis connection hostname | +| `port` | number | Redis port | +| `password` | string | Redis password (omitted with `--hide-credentials`) | +| `state` | string | `active`, `suspended`, or `passive` | +| `tls` | boolean | TLS enabled | +| `type` | string | `free`, `payg`, `pro`, or `paid` | +| `primary_region` | string | Primary region | +| `read_regions` | string[] | Read replica regions | +| `eviction` | boolean | Key eviction enabled | +| `auto_upgrade` | boolean | Auto version upgrade enabled | +| `daily_backup_enabled` | boolean | Daily backups enabled | +| `budget` | number | Monthly spend cap in cents | +| `creation_time` | number | Unix timestamp | + +--- + +## Team Commands + +```bash +upstash team list --json +upstash team create --name --json +upstash team create --name --copy-cc --json # Copy credit card to team +upstash team delete --dry-run --json +upstash team delete --json +upstash team members --json # List team members +upstash team add-member --team-id --member-email --role --json +upstash team remove-member --team-id --member-email --dry-run --json +upstash team remove-member --team-id --member-email --json +``` + +Member roles: `admin`, `dev`, `finance` + +### Team Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `team_id` | string | Unique team identifier | +| `team_name` | string | Team display name | +| `copy_cc` | boolean | Credit card copied to team | + +### TeamMember Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `team_id` | string | Team identifier | +| `member_email` | string | Member email address | +| `member_role` | string | `owner`, `admin`, `dev`, or `finance` | + +--- + +## Vector Commands + +```bash +upstash vector list --json +upstash vector get --json +upstash vector create --name --region --similarity-function --dimension-count --json +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg --json +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 --json +upstash vector delete --dry-run --json +upstash vector delete --json +upstash vector rename --name --json +upstash vector reset-password --json +upstash vector set-plan --plan --json # Plans: free, payg, fixed +upstash vector transfer --target-account --json +upstash vector stats --json # Aggregate stats across all indexes +upstash vector index-stats --json +upstash vector index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +``` + +### Available Vector Regions + +`eu-west-1`, `us-east-1`, `us-central1` + +### Similarity Functions + +`COSINE`, `EUCLIDEAN`, `DOT_PRODUCT` + +### Index Types + +`DENSE`, `SPARSE`, `HYBRID` + +### Embedding Models + +`BGE_SMALL_EN_V1_5`, `BGE_BASE_EN_V1_5`, `BGE_LARGE_EN_V1_5`, `BGE_M3` + +### Sparse Embedding Models + +`BM25`, `BGE_M3` + +### VectorIndex Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique index identifier | +| `name` | string | Index name | +| `region` | string | Deployment region | +| `similarity_function` | string | Distance metric | +| `dimension_count` | number | Dimensions per vector | +| `index_type` | string | `DENSE`, `SPARSE`, or `HYBRID` | +| `embedding_model` | string | Dense embedding model (if set) | +| `sparse_embedding_model` | string | Sparse embedding model (if set) | +| `endpoint` | string | REST endpoint hostname | +| `token` | string | Read-write auth token | +| `read_only_token` | string | Read-only auth token | +| `type` | string | `free`, `payg`, or `fixed` | +| `max_vector_count` | number | Vector capacity | +| `creation_time` | number | Unix timestamp | + +--- + +## Search Commands + +```bash +upstash search list --json +upstash search get --json +upstash search create --name --region --type --json +upstash search delete --dry-run --json +upstash search delete --json +upstash search rename --name --json +upstash search reset-password --json +upstash search transfer --target-account --json +upstash search stats --json # Aggregate stats across all indexes +upstash search index-stats --json +upstash search index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +``` + +### Available Search Regions + +`eu-west-1`, `us-central1` + +### Search Plans + +`free`, `payg`, `fixed` + +### SearchIndex Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique index identifier | +| `name` | string | Index name | +| `region` | string | Deployment region | +| `type` | string | `free`, `payg`, or `fixed` | +| `endpoint` | string | REST endpoint hostname | +| `token` | string | Read-write auth token | +| `read_only_token` | string | Read-only auth token | +| `input_enrichment_enabled` | boolean | Input enrichment enabled | +| `creation_time` | number | Unix timestamp | + +--- + +## QStash Commands + +```bash +upstash qstash list --json # All instances; map `region` → `id` for other commands +upstash qstash get --json +upstash qstash rotate-token --json +upstash qstash set-plan --plan --json # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m +upstash qstash stats --json +upstash qstash stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash qstash ipv4 --json # CIDR blocks for firewall allowlisting +upstash qstash move-to-team --qstash-id --target-team-id --json +upstash qstash update-budget --budget --json # 0 = no limit +upstash qstash enable-prodpack --json +upstash qstash disable-prodpack --json +``` + +### QStashUser Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | QStash instance identifier | +| `customer_id` | string | Owner email or team ID | +| `token` | string | Auth token for QStash API | +| `state` | string | `active` or `passive` | +| `type` | string | `free` or `paid` | +| `reserved_type` | string | Reserved plan: `paid`, `qstash_fixed_1m`, `qstash_fixed_10m`, `qstash_fixed_100m` | +| `region` | string | `eu-central-1` or `us-east-1` | +| `budget` | number | Monthly spend cap in dollars (0 = no limit) | +| `prod_pack_enabled` | boolean | Production pack active | +| `max_requests_per_day` | number | Daily request soft limit | +| `max_requests_per_second` | number | Rate limit | +| `max_topics` | number | Max topics | +| `max_schedules` | number | Max schedules | +| `max_queues` | number | Max queues | +| `timeout` | number | Request timeout in seconds | +| `creation_time` | number | Unix timestamp | + +--- + +## Tips for Agents + +- Always use `--json` for reliable output parsing. +- Exit code `0` = success, `1` = error. +- Use `--dry-run` before any `delete` or `remove-member` command to confirm the target. +- Use `--hide-credentials` on `redis get` when the password is not needed. +- Pipe `--json` output through `jq` for field extraction: + ```bash + upstash redis list --json | jq '.[].database_id' + upstash vector list --json | jq '.[] | {id, name, region}' + upstash qstash list --json | jq '.[] | {id, region}' + upstash team members --json | jq '.[].member_email' + ``` diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..9f26864 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,50 @@ +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { homedir } from "os"; +import { join } from "path"; + +const CONFIG_PATH = join(homedir(), ".upstash.json"); + +export interface Auth { + email: string; + apiKey: string; +} + +interface Config { + email?: string; + api_key?: string; +} + +function readConfig(): Config { + if (!existsSync(CONFIG_PATH)) return {}; + try { + return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as Config; + } catch { + return {}; + } +} + +export function saveAuth(email: string, apiKey: string): void { + const config: Config = { email, api_key: apiKey }; + writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 }); +} + +export function clearAuth(): void { + writeFileSync(CONFIG_PATH, JSON.stringify({}), { mode: 0o600 }); +} + +export function resolveAuth(flags: { email?: string; apiKey?: string }): Auth { + const config = readConfig(); + const email = flags.email ?? process.env.UPSTASH_EMAIL ?? config.email; + const apiKey = flags.apiKey ?? process.env.UPSTASH_API_KEY ?? config.api_key; + + if (!email || !apiKey) { + console.error( + "Error: Authentication required.\n" + + " Set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables, or\n" + + " run: upstash auth login --email --api-key ", + ); + process.exit(1); + } + + return { email, apiKey }; +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..5456d42 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import { registerAuth } from "./commands/auth/index.js"; +import { registerRedis } from "./commands/redis/index.js"; +import { registerTeam } from "./commands/team/index.js"; +import { registerVector } from "./commands/vector/index.js"; +import { registerSearch } from "./commands/search/index.js"; +import { registerQStash } from "./commands/qstash/index.js"; + +const program = new Command(); + +program + .name("upstash") + .description("Agent-friendly CLI for Upstash") + .version("1.0.0"); + +registerAuth(program); +registerRedis(program); +registerTeam(program); +registerVector(program); +registerSearch(program); +registerQStash(program); + +program.parse(); diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..331e329 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,29 @@ +import type { Auth } from "./auth.js"; + +const BASE_URL = "https://api.upstash.com"; + +export async function request( + auth: Auth, + method: string, + path: string, + body?: unknown, +): Promise { + const credentials = Buffer.from(`${auth.email}:${auth.apiKey}`).toString("base64"); + const response = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + Authorization: `Basic ${credentials}`, + "Content-Type": "application/json", + }, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + + const text = await response.text(); + + if (!response.ok) { + throw new Error(text || `HTTP ${response.status}`); + } + + if (text === "" || text === '"OK"') return "OK" as T; + return JSON.parse(text) as T; +} diff --git a/src/commands/auth/index.ts b/src/commands/auth/index.ts new file mode 100644 index 0000000..9a5369b --- /dev/null +++ b/src/commands/auth/index.ts @@ -0,0 +1,48 @@ +import { Command } from "commander"; +import { saveAuth, clearAuth, resolveAuth } from "../../auth.js"; +import { printJSON } from "../../output.js"; + +export function registerAuth(program: Command): void { + const auth = program.command("auth").description("Manage authentication credentials"); + + auth + .command("login") + .description("Save credentials to ~/.upstash.json") + .requiredOption("--email ", "Upstash email address") + .requiredOption("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action((flags: { email: string; apiKey: string; json?: boolean }) => { + saveAuth(flags.email, flags.apiKey); + if (flags.json) { + printJSON({ success: true, email: flags.email }); + return; + } + console.log(`Logged in as ${flags.email}`); + }); + + auth + .command("logout") + .description("Clear saved credentials from ~/.upstash.json") + .option("--json", "Output as JSON") + .action((flags: { json?: boolean }) => { + clearAuth(); + if (flags.json) { + printJSON({ success: true }); + return; + } + console.log("Logged out."); + }); + + auth + .command("whoami") + .description("Show the currently authenticated email") + .option("--json", "Output as JSON") + .action((flags: { json?: boolean }) => { + const creds = resolveAuth({}); + if (flags.json) { + printJSON({ email: creds.email }); + return; + } + console.log(`Logged in as ${creds.email}`); + }); +} diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts deleted file mode 100644 index 7a7ccfe..0000000 --- a/src/commands/auth/login.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Command } from "../../util/command.ts"; -import { DEFAULT_CONFIG_PATH, loadConfig } from "../../config.ts"; -import { cliffy } from "../../deps.ts"; -export const loginCmd = new Command() - .name("login") - .description( - `Log into your upstash account. -This will store your email and api key in ${DEFAULT_CONFIG_PATH}. -you can override this with "--config=/path/to/.upstash.json"`, - ) - .option("-e, --email=", "The email you use in upstash console") - .option( - "-k, --api-key=", - "Management api apiKey from https://console.upstash.com/account/api", - ) - .action((options): void => { - const config = loadConfig(options.config); - if (config) { - throw new Error( - `You are already logged in, please log out first or delete ${options.config}`, - ); - } - let email = options.upstashEmail; - if (!email) { - email = options.email; - } - - if (!email) { - throw new cliffy.ValidationError( - `email is missing, either use "--email" or set "UPSTASH_EMAIL" environment variable`, - ); - } - // if (!email) { - // if (options.ci) { - // throw new cliffy.ValidationError("email"); - // } - // email = await cliffy.Input.prompt("Enter your email"); - // } - - let apiKey = options.upstashApiKey; - - if (!apiKey) { - apiKey = options.apiKey; - } - if (!apiKey) { - throw new cliffy.ValidationError( - `api key is missing, either use "--api-key" or set "UPSTASH_API_KEY" environment variable`, - ); - } - // if (!apiKey) { - // if (options.ci) { - // throw new cliffy.ValidationError("apiKey"); - // } - // apiKey = await cliffy.Secret.prompt( - // "Enter your apiKey from https://console.upstash.com/account/api" - // ); - // } - - Deno.writeTextFileSync(options.config, JSON.stringify({ email, apiKey })); - }); diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts deleted file mode 100644 index 6ab8d73..0000000 --- a/src/commands/auth/logout.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Command } from "../../util/command.ts"; -import { deleteConfig } from "../../config.ts"; -export const logoutCmd = new Command() - .name("logout") - .description("Delete local configuration") - .action((options): void => { - try { - deleteConfig(options.config); - console.log("You have been logged out"); - } catch { - console.log("You were not logged in"); - } - }); diff --git a/src/commands/auth/mod.ts b/src/commands/auth/mod.ts deleted file mode 100644 index 92896bf..0000000 --- a/src/commands/auth/mod.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Command } from "../../util/command.ts"; -import { loginCmd } from "./login.ts"; -import { logoutCmd } from "./logout.ts"; -import { whoamiCmd } from "./whoami.ts"; -const authCmd = new Command(); - -authCmd - .description("Login and logout") - .command("login", loginCmd) - .command("logout", logoutCmd) - .command("whoami", whoamiCmd); - -authCmd.reset().action(() => { - authCmd.showHelp(); -}); - -export { authCmd }; diff --git a/src/commands/auth/whoami.ts b/src/commands/auth/whoami.ts deleted file mode 100644 index 5c7666c..0000000 --- a/src/commands/auth/whoami.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { loadConfig } from "../../config.ts"; -export const whoamiCmd = new Command() - .name("whoami") - .description("Return the current users email") - .action((options): void => { - const config = loadConfig(options.config); - if (!config) { - throw new Error("You are not logged in, please run `upstash auth login`"); - } - - console.log( - `Currently logged in as ${cliffy.colors.brightGreen(config.email)}`, - ); - }); diff --git a/src/commands/qstash/disable-prodpack.ts b/src/commands/qstash/disable-prodpack.ts new file mode 100644 index 0000000..767a0c7 --- /dev/null +++ b/src/commands/qstash/disable-prodpack.ts @@ -0,0 +1,25 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerQStashDisableProdpack(qstash: Command): void { + qstash + .command("disable-prodpack ") + .description("Disable the production pack for a QStash instance") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/qstash/disable-prodpack/${qstashId}`); + if (flags.json) { printJSON({ success: true, qstash_id: qstashId }); return; } + console.log("Production pack disabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/enable-prodpack.ts b/src/commands/qstash/enable-prodpack.ts new file mode 100644 index 0000000..1aa323a --- /dev/null +++ b/src/commands/qstash/enable-prodpack.ts @@ -0,0 +1,25 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerQStashEnableProdpack(qstash: Command): void { + qstash + .command("enable-prodpack ") + .description("Enable the production pack for a QStash instance") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/qstash/enable-prodpack/${qstashId}`); + if (flags.json) { printJSON({ success: true, qstash_id: qstashId }); return; } + console.log("Production pack enabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/get.ts b/src/commands/qstash/get.ts new file mode 100644 index 0000000..3781d9d --- /dev/null +++ b/src/commands/qstash/get.ts @@ -0,0 +1,26 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { QStashUser } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerQStashGet(qstash: Command): void { + qstash + .command("get ") + .description("Get details of a QStash instance") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const q = await request(auth, "GET", `/v2/qstash/user/${qstashId}`); + if (flags.json) { printJSON(q); return; } + printKeyValue(q as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/index.ts b/src/commands/qstash/index.ts new file mode 100644 index 0000000..da3c4c3 --- /dev/null +++ b/src/commands/qstash/index.ts @@ -0,0 +1,26 @@ +import { Command } from "commander"; +import { registerQStashGet } from "./get.js"; +import { registerQStashList } from "./list.js"; +import { registerQStashRotateToken } from "./rotate-token.js"; +import { registerQStashSetPlan } from "./set-plan.js"; +import { registerQStashStats } from "./stats.js"; +import { registerQStashIpv4 } from "./ipv4.js"; +import { registerQStashMoveToTeam } from "./move-to-team.js"; +import { registerQStashUpdateBudget } from "./update-budget.js"; +import { registerQStashEnableProdpack } from "./enable-prodpack.js"; +import { registerQStashDisableProdpack } from "./disable-prodpack.js"; + +export function registerQStash(program: Command): void { + const qstash = program.command("qstash").description("Manage QStash instances"); + + registerQStashGet(qstash); + registerQStashList(qstash); + registerQStashRotateToken(qstash); + registerQStashSetPlan(qstash); + registerQStashStats(qstash); + registerQStashIpv4(qstash); + registerQStashMoveToTeam(qstash); + registerQStashUpdateBudget(qstash); + registerQStashEnableProdpack(qstash); + registerQStashDisableProdpack(qstash); +} diff --git a/src/commands/qstash/ipv4.ts b/src/commands/qstash/ipv4.ts new file mode 100644 index 0000000..b33a748 --- /dev/null +++ b/src/commands/qstash/ipv4.ts @@ -0,0 +1,27 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerQStashIpv4(qstash: Command): void { + qstash + .command("ipv4") + .description("List IPv4 CIDR blocks used by QStash (for firewall allowlisting)") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const addresses = await request(auth, "GET", "/v2/qstash/ipv4"); + if (flags.json) { printJSON(addresses); return; } + for (const addr of addresses) { + console.log(addr); + } + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/list.ts b/src/commands/qstash/list.ts new file mode 100644 index 0000000..3446c6b --- /dev/null +++ b/src/commands/qstash/list.ts @@ -0,0 +1,40 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printTable, handleError } from "../../output.js"; +import type { QStashUser } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerQStashList(qstash: Command): void { + qstash + .command("list") + .description("List all QStash instances (id and region per deployment)") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const users = await request(auth, "GET", "/v2/qstash/users"); + if (flags.json) { + printJSON(users); + return; + } + if (users.length === 0) { + console.log("No QStash instances found."); + return; + } + printTable( + ["ID", "REGION", "STATE", "TYPE"], + users.map((u) => [u.id, u.region ?? "", u.state ?? "", u.type ?? ""]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/move-to-team.ts b/src/commands/qstash/move-to-team.ts new file mode 100644 index 0000000..69110f5 --- /dev/null +++ b/src/commands/qstash/move-to-team.ts @@ -0,0 +1,39 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + qstashId: string; + targetTeamId: string; +} + +export function registerQStashMoveToTeam(qstash: Command): void { + qstash + .command("move-to-team") + .description("Move a QStash instance to a team") + .requiredOption("--qstash-id ", "QStash instance ID") + .requiredOption("--target-team-id ", "Target team ID") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", "/v2/qstash/move-to-team", { + qstash_id: flags.qstashId, + target_team_id: flags.targetTeamId, + }); + if (flags.json) { + printJSON({ success: true, qstash_id: flags.qstashId, target_team_id: flags.targetTeamId }); + return; + } + console.log(`QStash ${flags.qstashId} moved to team ${flags.targetTeamId}.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/rotate-token.ts b/src/commands/qstash/rotate-token.ts new file mode 100644 index 0000000..8f33eb4 --- /dev/null +++ b/src/commands/qstash/rotate-token.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { QStashUser } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerQStashRotateToken(qstash: Command): void { + qstash + .command("rotate-token ") + .description("Reset the authentication token for a QStash instance") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const q = await request( + auth, + "POST", + `/v2/qstash/rotate-token/${qstashId}`, + ); + if (flags.json) { printJSON(q); return; } + console.log("Token rotated successfully."); + console.log(); + printKeyValue(q as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/set-plan.ts b/src/commands/qstash/set-plan.ts new file mode 100644 index 0000000..17cfe1b --- /dev/null +++ b/src/commands/qstash/set-plan.ts @@ -0,0 +1,29 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; +import { QSTASH_PLANS } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; plan: string } + +export function registerQStashSetPlan(qstash: Command): void { + qstash + .command("set-plan ") + .description(`Change the plan for a QStash instance. Plans: ${QSTASH_PLANS.join(", ")}`) + .requiredOption("--plan ", `Target plan (${QSTASH_PLANS.join(", ")})`) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/qstash/set-plan/${qstashId}`, { + plan_name: flags.plan, + }); + if (flags.json) { printJSON({ success: true, qstash_id: qstashId, plan: flags.plan }); return; } + console.log(`Plan changed to '${flags.plan}'.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/stats.ts b/src/commands/qstash/stats.ts new file mode 100644 index 0000000..64dd777 --- /dev/null +++ b/src/commands/qstash/stats.ts @@ -0,0 +1,35 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; +import { STATS_PERIODS } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; period?: string } + +export function registerQStashStats(qstash: Command): void { + qstash + .command("stats ") + .description("Get usage statistics for a QStash instance") + .option( + "--period ", + `Time period for aggregation. Available: ${STATS_PERIODS.join(", ")}`, + "1h", + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + const qs = flags.period ? `?period=${flags.period}` : ""; + try { + const stats = await request>( + auth, + "GET", + `/v2/qstash/stats/${qstashId}${qs}`, + ); + printJSON(stats); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/qstash/update-budget.ts b/src/commands/qstash/update-budget.ts new file mode 100644 index 0000000..c4a9224 --- /dev/null +++ b/src/commands/qstash/update-budget.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; budget: number } + +export function registerQStashUpdateBudget(qstash: Command): void { + qstash + .command("update-budget ") + .description("Update the monthly spend budget for a QStash instance (in dollars, 0 = no limit)") + .requiredOption("--budget ", "Monthly budget in dollars (0 = no limit)", parseInt) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (qstashId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "PATCH", `/v2/qstash/update-budget/${qstashId}`, { + budget: flags.budget, + }); + if (flags.json) { + printJSON({ success: true, qstash_id: qstashId, budget: flags.budget }); + return; + } + const label = flags.budget === 0 ? "no limit" : `$${flags.budget}/month`; + console.log(`Budget updated to ${label}.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/backup/create.ts b/src/commands/redis/backup/create.ts new file mode 100644 index 0000000..bf09e9a --- /dev/null +++ b/src/commands/redis/backup/create.ts @@ -0,0 +1,36 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../../auth.js"; +import { request } from "../../../client.js"; +import { printJSON, handleError } from "../../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + name: string; +} + +export function registerBackupCreate(backup: Command): void { + backup + .command("create ") + .description("Create a backup of a Redis database") + .requiredOption("--name ", "Backup name") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/create-backup/${databaseId}`, { + name: flags.name, + }); + if (flags.json) { + printJSON({ success: true, database_id: databaseId, name: flags.name }); + return; + } + console.log(`Backup '${flags.name}' created.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/backup/delete.ts b/src/commands/redis/backup/delete.ts new file mode 100644 index 0000000..0212b84 --- /dev/null +++ b/src/commands/redis/backup/delete.ts @@ -0,0 +1,53 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../../auth.js"; +import { request } from "../../../client.js"; +import { printJSON, handleError } from "../../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + dryRun?: boolean; +} + +export function registerBackupDelete(backup: Command): void { + backup + .command("delete ") + .description("Delete a backup of a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--dry-run", "Preview the action without executing it") + .action(async (databaseId: string, backupId: string, flags: Flags) => { + if (flags.dryRun) { + const preview = { + action: "delete-backup", + database_id: databaseId, + backup_id: backupId, + dry_run: true, + }; + if (flags.json) { + printJSON(preview); + return; + } + console.log(`Dry run: would delete backup ${backupId} from database ${databaseId}`); + return; + } + + const auth = resolveAuth(flags); + try { + await request( + auth, + "DELETE", + `/v2/redis/delete-backup/${databaseId}/${backupId}`, + ); + if (flags.json) { + printJSON({ deleted: true, backup_id: backupId }); + return; + } + console.log(`Backup ${backupId} deleted.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/backup/disable-daily.ts b/src/commands/redis/backup/disable-daily.ts new file mode 100644 index 0000000..6ab764a --- /dev/null +++ b/src/commands/redis/backup/disable-daily.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../../auth.js"; +import { request } from "../../../client.js"; +import { printJSON, handleError } from "../../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerDisableDaily(backup: Command): void { + backup + .command("disable-daily ") + .description("Disable daily automatic backups for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/disable-dailybackup/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("Daily backup disabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/backup/enable-daily.ts b/src/commands/redis/backup/enable-daily.ts new file mode 100644 index 0000000..feec3cf --- /dev/null +++ b/src/commands/redis/backup/enable-daily.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../../auth.js"; +import { request } from "../../../client.js"; +import { printJSON, handleError } from "../../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerEnableDaily(backup: Command): void { + backup + .command("enable-daily ") + .description("Enable daily automatic backups for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/enable-dailybackup/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("Daily backup enabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/backup/index.ts b/src/commands/redis/backup/index.ts new file mode 100644 index 0000000..c74175c --- /dev/null +++ b/src/commands/redis/backup/index.ts @@ -0,0 +1,18 @@ +import { Command } from "commander"; +import { registerBackupList } from "./list.js"; +import { registerBackupCreate } from "./create.js"; +import { registerBackupDelete } from "./delete.js"; +import { registerBackupRestore } from "./restore.js"; +import { registerEnableDaily } from "./enable-daily.js"; +import { registerDisableDaily } from "./disable-daily.js"; + +export function registerBackup(redis: Command): void { + const backup = redis.command("backup").description("Manage Redis database backups"); + + registerBackupList(backup); + registerBackupCreate(backup); + registerBackupDelete(backup); + registerBackupRestore(backup); + registerEnableDaily(backup); + registerDisableDaily(backup); +} diff --git a/src/commands/redis/backup/list.ts b/src/commands/redis/backup/list.ts new file mode 100644 index 0000000..1d69695 --- /dev/null +++ b/src/commands/redis/backup/list.ts @@ -0,0 +1,48 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../../auth.js"; +import { request } from "../../../client.js"; +import { printJSON, printTable, handleError } from "../../../output.js"; +import type { Backup } from "../../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerBackupList(backup: Command): void { + backup + .command("list ") + .description("List all backups for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const backups = await request( + auth, + "GET", + `/v2/redis/list-backup/${databaseId}`, + ); + if (flags.json) { + printJSON(backups); + return; + } + if (backups.length === 0) { + console.log("No backups found."); + return; + } + printTable( + ["BACKUP_ID", "NAME", "CREATED"], + backups.map((b) => [ + b.backup_id, + b.backup_name, + new Date(b.creation_time * 1000).toISOString(), + ]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/backup/restore.ts b/src/commands/redis/backup/restore.ts new file mode 100644 index 0000000..c423414 --- /dev/null +++ b/src/commands/redis/backup/restore.ts @@ -0,0 +1,42 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../../auth.js"; +import { request } from "../../../client.js"; +import { printJSON, handleError } from "../../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + backupId: string; +} + +export function registerBackupRestore(backup: Command): void { + backup + .command("restore ") + .description("Restore a Redis database from a backup") + .requiredOption("--backup-id ", "ID of the backup to restore from") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/restore-backup/${databaseId}`, { + backup_id: flags.backupId, + }); + if (flags.json) { + printJSON({ + success: true, + database_id: databaseId, + backup_id: flags.backupId, + }); + return; + } + console.log( + `Restore started for database ${databaseId} from backup ${flags.backupId}.`, + ); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/change-plan.ts b/src/commands/redis/change-plan.ts new file mode 100644 index 0000000..7c51923 --- /dev/null +++ b/src/commands/redis/change-plan.ts @@ -0,0 +1,38 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +const PLANS = ["free", "payg", "pro", "paid"]; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + plan: string; +} + +export function registerChangePlan(redis: Command): void { + redis + .command("change-plan ") + .description(`Change the pricing plan of a Redis database. Plans: ${PLANS.join(", ")}`) + .requiredOption("--plan ", `Plan type (${PLANS.join(", ")})`) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/change-plan/${databaseId}`, { + plan: flags.plan, + }); + if (flags.json) { + printJSON({ success: true, database_id: databaseId, plan: flags.plan }); + return; + } + console.log(`Plan changed to '${flags.plan}'.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/create.ts b/src/commands/redis/create.ts index 51e1301..a55b47e 100644 --- a/src/commands/redis/create.ts +++ b/src/commands/redis/create.ts @@ -1,113 +1,57 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -import type { Database } from "./types.ts"; -export enum Region { - "us-west-1" = "us-west-1", - "us-west-2" = "us-west-2", - "us-east-1" = "us-east-1", - "us-central1" = "us-central1", - - "eu-west-1" = "eu-west-1", - "eu-central-1" = "eu-central-1", - - "ap-south-1" = "ap-south-1", - "ap-southeast-1" = "ap-southeast-1", - "ap-southeast-2" = "ap-southeast-2", - - "ap-northeast-1" = "ap-northeast-1", - - "sa-east-1" = "sa-east-1", +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { REGIONS } from "../../types.js"; +import type { Database } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + name: string; + region: string; + readRegions?: string[]; } -export const createCmd = new Command() - .name("create") - .description("Create a new redis database") - .option("-n --name ", "Name of the database", { required: true }) - .type("region", new cliffy.EnumType(Region)) - .option("-r --region ", "Primary region of the database", { - required: true, - }) - .option( - "--read-regions [regions...:region]", - "Read regions of the database", - { - required: false, - }, - ) - .example("global", "upstash redis create --name mydb --region=us-east-1") - .example( - "with replication", - "upstash redis create --name mydb --region=us-east-1 --read-regions=us-west-1 eu-west-1", - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - // if (!options.name) { - // if (options.ci) { - // throw new cliffy.ValidationError("name"); - // } - // options.name = await cliffy.Input.prompt("Set a name for your database"); - // } - - // if (!options.region) { - // if (options.ci) { - // throw new cliffy.ValidationError("region"); - // } - // options.region = (await cliffy.Select.prompt({ - // message: "Select a region", - // options: Object.entries(Region).map(([name, value]) => ({ - // name, - // value, - // })), - // })) as Region; - // } - const body: Record< - string, - string | number | boolean | undefined | string[] - > = { - name: options.name, - region: "global", - primary_region: options.region, - read_regions: options.readRegions, - }; - - if (body.read_regions !== undefined && !Array.isArray(body.read_regions)) { - throw new Error("--read_regions should be an array"); - } - - const db = await http.request({ - method: "POST", - authorization, - path: ["v2", "redis", "database"], - body, +export function registerCreate(redis: Command): void { + redis + .command("create") + .description("Create a new Redis database") + .requiredOption("--name ", "Database name") + .requiredOption( + "--region ", + `Primary region. Available: ${REGIONS.join(", ")}`, + ) + .option("--read-regions ", "Read replica regions (space-separated)") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + if (!(REGIONS as readonly string[]).includes(flags.region)) { + console.error( + `Error: Invalid region '${flags.region}'.\nAvailable: ${REGIONS.join(", ")}`, + ); + process.exit(1); + } + + const auth = resolveAuth(flags); + try { + const db = await request(auth, "POST", "/v2/redis/database", { + database_name: flags.name, + region: "global", + primary_region: flags.region, + read_regions: flags.readRegions, + }); + if (flags.json) { + printJSON(db); + return; + } + console.log(`Database '${db.database_name}' created.`); + console.log(); + printKeyValue(db as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify(db, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Database has been created")); - console.log(); - console.log( - cliffy.Table.from( - Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - console.log(); - console.log(); - - console.log( - "You can visit your database details page: " + - cliffy.colors.yellow( - "https://console.upstash.com/redis/" + db.database_id, - ), - ); - console.log(); - console.log( - "Connect to your database with redis-cli: " + - cliffy.colors.yellow( - `redis-cli --tls -u redis://${db.password}@${db.endpoint}:${db.port}`, - ), - ); - }); +} diff --git a/src/commands/redis/delete.ts b/src/commands/redis/delete.ts index a066c7b..ba84fa1 100644 --- a/src/commands/redis/delete.ts +++ b/src/commands/redis/delete.ts @@ -1,46 +1,44 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -// import type { Database } from "./types.ts"; -export const deleteCmd = new Command() - .name("delete") - .description("delete a redis database") - .option("--id=", "The uuid of the cluster", { required: true }) - .example( - "Delete", - `upstash redis delete --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const dbs = await http.request({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to delete", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + dryRun?: boolean; +} - await http.request({ - method: "DELETE", - authorization, - path: ["v2", "redis", "database", options.id!], +export function registerDelete(redis: Command): void { + redis + .command("delete ") + .description("Delete a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--dry-run", "Preview the action without executing it") + .action(async (databaseId: string, flags: Flags) => { + if (flags.dryRun) { + const preview = { action: "delete", database_id: databaseId, dry_run: true }; + if (flags.json) { + printJSON(preview); + return; + } + console.log(`Dry run: would delete database ${databaseId}`); + return; + } + + const auth = resolveAuth(flags); + try { + await request(auth, "DELETE", `/v2/redis/database/${databaseId}`); + if (flags.json) { + printJSON({ deleted: true, database_id: databaseId }); + return; + } + console.log(`Database ${databaseId} deleted.`); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify({ ok: true }, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Database has been deleted")); - console.log(); - }); +} diff --git a/src/commands/redis/disable-autoupgrade.ts b/src/commands/redis/disable-autoupgrade.ts new file mode 100644 index 0000000..8ca8bd5 --- /dev/null +++ b/src/commands/redis/disable-autoupgrade.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerDisableAutoupgrade(redis: Command): void { + redis + .command("disable-autoupgrade ") + .description("Disable automatic version upgrades for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/disable-autoupgrade/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("Auto-upgrade disabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/disable-eviction.ts b/src/commands/redis/disable-eviction.ts new file mode 100644 index 0000000..273a765 --- /dev/null +++ b/src/commands/redis/disable-eviction.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerDisableEviction(redis: Command): void { + redis + .command("disable-eviction ") + .description("Disable key eviction for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/disable-eviction/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("Eviction disabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/enable-autoupgrade.ts b/src/commands/redis/enable-autoupgrade.ts new file mode 100644 index 0000000..60fb7d0 --- /dev/null +++ b/src/commands/redis/enable-autoupgrade.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerEnableAutoupgrade(redis: Command): void { + redis + .command("enable-autoupgrade ") + .description("Enable automatic version upgrades for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/enable-autoupgrade/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("Auto-upgrade enabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/enable-eviction.ts b/src/commands/redis/enable-eviction.ts new file mode 100644 index 0000000..1851a64 --- /dev/null +++ b/src/commands/redis/enable-eviction.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerEnableEviction(redis: Command): void { + redis + .command("enable-eviction ") + .description("Enable key eviction for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/enable-eviction/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("Eviction enabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/enable-tls.ts b/src/commands/redis/enable-tls.ts new file mode 100644 index 0000000..bf179c5 --- /dev/null +++ b/src/commands/redis/enable-tls.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerEnableTls(redis: Command): void { + redis + .command("enable-tls ") + .description("Enable TLS for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/enable-tls/${databaseId}`); + if (flags.json) { + printJSON({ success: true, database_id: databaseId }); + return; + } + console.log("TLS enabled."); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/enable_multizone_replication.ts b/src/commands/redis/enable_multizone_replication.ts deleted file mode 100644 index ab64473..0000000 --- a/src/commands/redis/enable_multizone_replication.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; - -export const enableMultizoneReplicationCmd = new Command() - .name("enable-multizone-replication") - .description("Enable multizone replication for a redis database") - .option("--id=", "The id of your database", { required: true }) - .example( - "Enable", - `upstash redis enable-multizone-replication f860e7e2-27b8-4166-90d5-ea41e90b4809`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const dbs = await http.request< - // { database_name: string; database_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to rename", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } - - const db = await http.request({ - method: "POST", - authorization, - path: ["v2", "redis", "enable-multizone", options.id!], - }); - if (options.json) { - console.log(JSON.stringify(db, null, 2)); - return; - } - console.log( - cliffy.colors.brightGreen("Multizone replication has been enabled"), - ); - }); diff --git a/src/commands/redis/get.ts b/src/commands/redis/get.ts index f1ce45c..b0b75c6 100644 --- a/src/commands/redis/get.ts +++ b/src/commands/redis/get.ts @@ -1,50 +1,36 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -import type { Database } from "./types.ts"; +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { Database } from "../../types.js"; -export const getCmd = new Command() - .name("get") - .description("get a redis database") - .option("--id=", "The id of your database", { required: true }) - .example("Get", `upstash redis get --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`) - .action(async (options): Promise => { - const authorization = await parseAuth(options); +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + hideCredentials?: boolean; +} - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const dbs = await http.request< - // { database_name: string; database_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to delete", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } - - const db = await http.request({ - method: "GET", - authorization, - path: ["v2", "redis", "database", options.id!], +export function registerGet(redis: Command): void { + redis + .command("get ") + .description("Get details of a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--hide-credentials", "Omit password from output") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + const qs = flags.hideCredentials ? "?credentials=hide" : ""; + try { + const db = await request(auth, "GET", `/v2/redis/database/${databaseId}${qs}`); + if (flags.json) { + printJSON(db); + return; + } + printKeyValue(db as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify(db, null, 2)); - return; - } - - console.log( - cliffy.Table.from( - Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - }); +} diff --git a/src/commands/redis/index.ts b/src/commands/redis/index.ts new file mode 100644 index 0000000..ff95988 --- /dev/null +++ b/src/commands/redis/index.ts @@ -0,0 +1,40 @@ +import { Command } from "commander"; +import { registerCreate } from "./create.js"; +import { registerList } from "./list.js"; +import { registerGet } from "./get.js"; +import { registerDelete } from "./delete.js"; +import { registerRename } from "./rename.js"; +import { registerResetPassword } from "./reset-password.js"; +import { registerStats } from "./stats.js"; +import { registerEnableTls } from "./enable-tls.js"; +import { registerEnableEviction } from "./enable-eviction.js"; +import { registerDisableEviction } from "./disable-eviction.js"; +import { registerEnableAutoupgrade } from "./enable-autoupgrade.js"; +import { registerDisableAutoupgrade } from "./disable-autoupgrade.js"; +import { registerChangePlan } from "./change-plan.js"; +import { registerUpdateBudget } from "./update-budget.js"; +import { registerUpdateRegions } from "./update-regions.js"; +import { registerMoveToTeam } from "./move-to-team.js"; +import { registerBackup } from "./backup/index.js"; + +export function registerRedis(program: Command): void { + const redis = program.command("redis").description("Manage Redis databases"); + + registerCreate(redis); + registerList(redis); + registerGet(redis); + registerDelete(redis); + registerRename(redis); + registerResetPassword(redis); + registerStats(redis); + registerEnableTls(redis); + registerEnableEviction(redis); + registerDisableEviction(redis); + registerEnableAutoupgrade(redis); + registerDisableAutoupgrade(redis); + registerChangePlan(redis); + registerUpdateBudget(redis); + registerUpdateRegions(redis); + registerMoveToTeam(redis); + registerBackup(redis); +} diff --git a/src/commands/redis/list.ts b/src/commands/redis/list.ts index 5b27d99..60a3a8a 100644 --- a/src/commands/redis/list.ts +++ b/src/commands/redis/list.ts @@ -1,47 +1,47 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -import type { Database } from "./types.ts"; -export const listCmd = new Command() - .name("list") - .description("list all your databases") - .option("-e, --expanded ", "Show expanded information") - .example("List", "upstash redis list") - .action(async (options): Promise => { - const authorization = await parseAuth(options); - const dbs = await http.request({ - method: "GET", - authorization, - path: ["v2", "redis", "databases"], - }); - if (options.json) { - console.log(JSON.stringify(dbs, null, 2)); - return; - } - // if (!options.expanded) { - // console.log( - // cliffy.Table.from( - // dbs.map((db) => [db.database_name, db.database_id]), - // ).toString(), - // ); - // return; - // } +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printTable, handleError } from "../../output.js"; +import type { Database } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} - dbs.forEach((db) => { - console.log(); - console.log(); - console.log( - cliffy.colors.underline(cliffy.colors.brightGreen(db.database_name)), - ); - console.log(); - console.log( - cliffy.Table.from( - Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - console.log(); +export function registerList(redis: Command): void { + redis + .command("list") + .description("List all Redis databases") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const dbs = await request(auth, "GET", "/v2/redis/databases"); + if (flags.json) { + printJSON(dbs); + return; + } + if (dbs.length === 0) { + console.log("No databases found."); + return; + } + printTable( + ["ID", "NAME", "STATE", "REGION", "TYPE", "TLS"], + dbs.map((db) => [ + db.database_id, + db.database_name, + db.state ?? "", + db.primary_region ?? db.region ?? "", + db.type ?? "", + db.tls ? "yes" : "no", + ]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - console.log(); - console.log(); - }); +} diff --git a/src/commands/redis/mod.ts b/src/commands/redis/mod.ts deleted file mode 100644 index 81d8415..0000000 --- a/src/commands/redis/mod.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Command } from "../../util/command.ts"; -import { createCmd } from "./create.ts"; -import { listCmd } from "./list.ts"; -import { deleteCmd } from "./delete.ts"; -import { getCmd } from "./get.ts"; -import { statsCmd } from "./stats.ts"; -import { resetPasswordCmd } from "./reset_password.ts"; -import { renameCmd } from "./rename.ts"; -import { enableMultizoneReplicationCmd } from "./enable_multizone_replication.ts"; -import { moveToTeamCmd } from "./move_to_team.ts"; -const redisCmd = new Command() - .description("Manage redis database instances") - .globalOption("--json=[boolean:boolean]", "Return raw json response") - .command("create", createCmd) - .command("list", listCmd) - .command("get", getCmd) - .command("delete", deleteCmd) - .command("stats", statsCmd) - .command("rename", renameCmd) - .command("reset-password", resetPasswordCmd) - .command("enable-multizone-replication", enableMultizoneReplicationCmd) - .command("move-to-team", moveToTeamCmd); - -redisCmd.reset().action(() => { - redisCmd.showHelp(); -}); - -export { redisCmd }; diff --git a/src/commands/redis/move-to-team.ts b/src/commands/redis/move-to-team.ts new file mode 100644 index 0000000..e64a0c8 --- /dev/null +++ b/src/commands/redis/move-to-team.ts @@ -0,0 +1,37 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + teamId: string; +} + +export function registerMoveToTeam(redis: Command): void { + redis + .command("move-to-team ") + .description("Move a Redis database to a team account") + .requiredOption("--team-id ", "Target team ID") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/move-to-team`, { + database_id: databaseId, + team_id: flags.teamId, + }); + if (flags.json) { + printJSON({ success: true, database_id: databaseId, team_id: flags.teamId }); + return; + } + console.log(`Database ${databaseId} moved to team ${flags.teamId}.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/move_to_team.ts b/src/commands/redis/move_to_team.ts deleted file mode 100644 index 0f7c457..0000000 --- a/src/commands/redis/move_to_team.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; - -export const moveToTeamCmd = new Command() - .name("move-to-team") - .description("Move a redis database to another team") - .option("--id=", "The id of your database", { required: true }) - .option("--team-id=", "The id of a team", { required: true }) - .example("Move", `upstash redis move-to-team`) - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const dbs = await http.request< - // { database_name: string; database_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to move", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } - // if (!options.teamId) { - // const teams = await http.request< - // { team_name: string; team_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "teams"], - // }); - // options.teamId = await cliffy.Select.prompt({ - // message: "Select the new team", - // options: teams.map(({ team_name, team_id }) => ({ - // name: team_name, - // value: team_id, - // })), - // }); - // } - - const db = await http.request({ - method: "POST", - authorization, - path: ["v2", "redis", "move-to-team"], - body: { - database_id: options.id, - team_id: options.teamId, - }, - }); - if (options.json) { - console.log(JSON.stringify(db, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Database has been moved")); - }); diff --git a/src/commands/redis/rename.ts b/src/commands/redis/rename.ts index 6a31a51..9b2dddd 100644 --- a/src/commands/redis/rename.ts +++ b/src/commands/redis/rename.ts @@ -1,58 +1,39 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -export const renameCmd = new Command() - .name("rename") - .description("Rename a redis database") - .option("--id=", "The id of your database", { required: true }) - .option("--name=", "Choose a new name", { required: true }) - .example( - "Rename", - `upstash redis rename f860e7e2-27b8-4166-90d5-ea41e90b4809`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { Database } from "../../types.js"; - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const dbs = await http.request< - // { database_name: string; database_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to rename", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + name: string; +} - // if (!options.name) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // options.name = await cliffy.Input.prompt({ - // message: "Choose a new name", - // }); - // } - const db = await http.request({ - method: "POST", - authorization, - path: ["v2", "redis", "rename", options.id!], - body: { - name: options.name, - }, +export function registerRename(redis: Command): void { + redis + .command("rename ") + .description("Rename a Redis database") + .requiredOption("--name ", "New database name") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const db = await request(auth, "POST", `/v2/redis/rename/${databaseId}`, { + name: flags.name, + }); + if (flags.json) { + printJSON(db); + return; + } + console.log(`Database renamed to '${db.database_name}'.`); + console.log(); + printKeyValue(db as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify(db, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Database has been renamed")); - }); +} diff --git a/src/commands/redis/reset-password.ts b/src/commands/redis/reset-password.ts new file mode 100644 index 0000000..ddb9cb0 --- /dev/null +++ b/src/commands/redis/reset-password.ts @@ -0,0 +1,39 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { Database } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerResetPassword(redis: Command): void { + redis + .command("reset-password ") + .description("Reset the password of a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const db = await request( + auth, + "POST", + `/v2/redis/reset-password/${databaseId}`, + ); + if (flags.json) { + printJSON(db); + return; + } + console.log("Password reset successfully."); + console.log(); + printKeyValue(db as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/reset_password.ts b/src/commands/redis/reset_password.ts deleted file mode 100644 index b50a15e..0000000 --- a/src/commands/redis/reset_password.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; - -import type { Database } from "./types.ts"; - -export const resetPasswordCmd = new Command() - .name("reset-password") - .description("reset the password of a redis database") - .option("--id=", "The id of your database", { required: true }) - .example( - "Reset", - `upstash redis reset-password --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const dbs = await http.request< - // { database_name: string; database_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to delete", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } - - const db = await http.request({ - method: "POST", - authorization, - path: ["v2", "redis", "reset-password", options.id!], - }); - if (options.json) { - console.log(JSON.stringify(db, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Database password has been reset")); - console.log(); - console.log( - cliffy.Table.from( - Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - }); diff --git a/src/commands/redis/stats.ts b/src/commands/redis/stats.ts index 4006ecb..7c3c151 100644 --- a/src/commands/redis/stats.ts +++ b/src/commands/redis/stats.ts @@ -1,41 +1,32 @@ -// import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -import type { Database } from "./types.ts"; -export const statsCmd = new Command() - .name("stats") - .description("Returns detailed information about the databse usage") - .option("--id=", "The id of your database", { required: true }) - .example( - "Get", - `upstash redis stats --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; - // if (!options.id) { - // const dbs = await http.request< - // { database_name: string; database_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "redis", "databases"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a database to delete", - // options: dbs.map(({ database_name, database_id }) => ({ - // name: database_name, - // value: database_id, - // })), - // }); - // } +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} - const db = await http.request({ - method: "GET", - authorization, - path: ["v2", "redis", "stats", options.id!], +export function registerStats(redis: Command): void { + redis + .command("stats ") + .description("Get usage statistics for a Redis database") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const stats = await request>( + auth, + "GET", + `/v2/redis/stats/${databaseId}`, + ); + printJSON(stats); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - console.log(JSON.stringify(db, null, 2)); - return; - }); +} diff --git a/src/commands/redis/types.ts b/src/commands/redis/types.ts deleted file mode 100644 index ef05358..0000000 --- a/src/commands/redis/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Database = { - database_id: string; - database_name: string; - password: string; - endpoint: string; - port: number; - - region: string; -}; diff --git a/src/commands/redis/update-budget.ts b/src/commands/redis/update-budget.ts new file mode 100644 index 0000000..e2b44e5 --- /dev/null +++ b/src/commands/redis/update-budget.ts @@ -0,0 +1,36 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + budget: number; +} + +export function registerUpdateBudget(redis: Command): void { + redis + .command("update-budget ") + .description("Update the monthly spend budget for a Redis database (in cents)") + .requiredOption("--budget ", "Monthly budget in cents", parseInt) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "PATCH", `/v2/redis/update-budget/${databaseId}`, { + budget: flags.budget, + }); + if (flags.json) { + printJSON({ success: true, database_id: databaseId, budget: flags.budget }); + return; + } + console.log(`Budget updated to ${flags.budget} cents/month.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/redis/update-regions.ts b/src/commands/redis/update-regions.ts new file mode 100644 index 0000000..20fd5a0 --- /dev/null +++ b/src/commands/redis/update-regions.ts @@ -0,0 +1,44 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; +import { REGIONS } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + readRegions: string[]; +} + +export function registerUpdateRegions(redis: Command): void { + redis + .command("update-regions ") + .description("Update read replica regions for a Redis database") + .requiredOption( + "--read-regions ", + `Read replica regions (space-separated). Available: ${REGIONS.join(", ")}`, + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (databaseId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/redis/update-regions/${databaseId}`, { + read_regions: flags.readRegions, + }); + if (flags.json) { + printJSON({ + success: true, + database_id: databaseId, + read_regions: flags.readRegions, + }); + return; + } + console.log(`Read regions updated: ${flags.readRegions.join(", ")}`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/create.ts b/src/commands/search/create.ts new file mode 100644 index 0000000..829fe4f --- /dev/null +++ b/src/commands/search/create.ts @@ -0,0 +1,49 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { SEARCH_REGIONS, SEARCH_PLANS } from "../../types.js"; +import type { SearchIndex } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + name: string; + region: string; + type: string; +} + +export function registerSearchCreate(search: Command): void { + search + .command("create") + .description("Create a new search index") + .requiredOption("--name ", "Index name") + .requiredOption( + "--region ", + `Region. Available: ${SEARCH_REGIONS.join(", ")}`, + ) + .requiredOption( + "--type ", + `Plan type. Available: ${SEARCH_PLANS.join(", ")}`, + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request(auth, "POST", "/v2/search", { + name: flags.name, + region: flags.region, + type: flags.type, + }); + if (flags.json) { printJSON(idx); return; } + console.log(`Search index '${idx.name}' created.`); + console.log(); + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/delete.ts b/src/commands/search/delete.ts new file mode 100644 index 0000000..2010f0d --- /dev/null +++ b/src/commands/search/delete.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; dryRun?: boolean } + +export function registerSearchDelete(search: Command): void { + search + .command("delete ") + .description("Delete a search index") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--dry-run", "Preview the action without executing it") + .action(async (indexId: string, flags: Flags) => { + if (flags.dryRun) { + const preview = { action: "delete", index_id: indexId, dry_run: true }; + if (flags.json) { printJSON(preview); return; } + console.log(`Dry run: would delete search index ${indexId}`); + return; + } + const auth = resolveAuth(flags); + try { + await request(auth, "DELETE", `/v2/search/${indexId}`); + if (flags.json) { printJSON({ deleted: true, index_id: indexId }); return; } + console.log(`Search index ${indexId} deleted.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/get.ts b/src/commands/search/get.ts new file mode 100644 index 0000000..2683586 --- /dev/null +++ b/src/commands/search/get.ts @@ -0,0 +1,26 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { SearchIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerSearchGet(search: Command): void { + search + .command("get ") + .description("Get details of a search index") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request(auth, "GET", `/v2/search/${indexId}`); + if (flags.json) { printJSON(idx); return; } + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/index.ts b/src/commands/search/index.ts new file mode 100644 index 0000000..4704e4b --- /dev/null +++ b/src/commands/search/index.ts @@ -0,0 +1,22 @@ +import { Command } from "commander"; +import { registerSearchList } from "./list.js"; +import { registerSearchCreate } from "./create.js"; +import { registerSearchGet } from "./get.js"; +import { registerSearchDelete } from "./delete.js"; +import { registerSearchRename } from "./rename.js"; +import { registerSearchResetPassword } from "./reset-password.js"; +import { registerSearchTransfer } from "./transfer.js"; +import { registerSearchStats } from "./stats.js"; + +export function registerSearch(program: Command): void { + const search = program.command("search").description("Manage Search indexes"); + + registerSearchList(search); + registerSearchCreate(search); + registerSearchGet(search); + registerSearchDelete(search); + registerSearchRename(search); + registerSearchResetPassword(search); + registerSearchTransfer(search); + registerSearchStats(search); +} diff --git a/src/commands/search/list.ts b/src/commands/search/list.ts new file mode 100644 index 0000000..3393f76 --- /dev/null +++ b/src/commands/search/list.ts @@ -0,0 +1,30 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printTable, handleError } from "../../output.js"; +import type { SearchIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerSearchList(search: Command): void { + search + .command("list") + .description("List all search indexes") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const indexes = await request(auth, "GET", "/v2/search"); + if (flags.json) { printJSON(indexes); return; } + if (indexes.length === 0) { console.log("No search indexes found."); return; } + printTable( + ["ID", "NAME", "REGION", "TYPE", "ENDPOINT"], + indexes.map((i) => [i.id, i.name, i.region, i.type, i.endpoint]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/rename.ts b/src/commands/search/rename.ts new file mode 100644 index 0000000..44fac98 --- /dev/null +++ b/src/commands/search/rename.ts @@ -0,0 +1,34 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { SearchIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; name: string } + +export function registerSearchRename(search: Command): void { + search + .command("rename ") + .description("Rename a search index") + .requiredOption("--name ", "New index name") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request( + auth, + "POST", + `/v2/search/${indexId}/rename`, + { name: flags.name }, + ); + if (flags.json) { printJSON(idx); return; } + console.log(`Index renamed to '${idx.name}'.`); + console.log(); + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/reset-password.ts b/src/commands/search/reset-password.ts new file mode 100644 index 0000000..685b528 --- /dev/null +++ b/src/commands/search/reset-password.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { SearchIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerSearchResetPassword(search: Command): void { + search + .command("reset-password ") + .description("Reset tokens for a search index") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request( + auth, + "POST", + `/v2/search/${indexId}/reset-password`, + ); + if (flags.json) { printJSON(idx); return; } + console.log("Tokens reset successfully."); + console.log(); + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/stats.ts b/src/commands/search/stats.ts new file mode 100644 index 0000000..c934e24 --- /dev/null +++ b/src/commands/search/stats.ts @@ -0,0 +1,51 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; +import { STATS_PERIODS } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; period?: string } + +export function registerSearchStats(search: Command): void { + search + .command("stats") + .description("Get statistics across all search indexes") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const stats = await request>(auth, "GET", "/v2/search/stats"); + printJSON(stats); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); + + search + .command("index-stats ") + .description("Get statistics for a specific search index") + .option( + "--period ", + `Time period for aggregation. Available: ${STATS_PERIODS.join(", ")}`, + "1h", + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + const qs = flags.period ? `?period=${flags.period}` : ""; + try { + const stats = await request>( + auth, + "GET", + `/v2/search/${indexId}/stats${qs}`, + ); + printJSON(stats); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/search/transfer.ts b/src/commands/search/transfer.ts new file mode 100644 index 0000000..004a896 --- /dev/null +++ b/src/commands/search/transfer.ts @@ -0,0 +1,31 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; targetAccount: string } + +export function registerSearchTransfer(search: Command): void { + search + .command("transfer ") + .description("Transfer a search index to another team") + .requiredOption("--target-account ", "Target team ID") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/search/${indexId}/transfer`, { + target_account: flags.targetAccount, + }); + if (flags.json) { + printJSON({ success: true, index_id: indexId, target_account: flags.targetAccount }); + return; + } + console.log(`Index ${indexId} transferred to team ${flags.targetAccount}.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/team/add-member.ts b/src/commands/team/add-member.ts new file mode 100644 index 0000000..22e4e5d --- /dev/null +++ b/src/commands/team/add-member.ts @@ -0,0 +1,56 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { TEAM_MEMBER_ROLES } from "../../types.js"; +import type { TeamMember } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + teamId: string; + memberEmail: string; + role: string; +} + +export function registerTeamAddMember(team: Command): void { + team + .command("add-member") + .description("Add a member to a team") + .requiredOption("--team-id ", "Team ID") + .requiredOption("--member-email ", "Email of the member to add") + .requiredOption( + "--role ", + `Member role (${TEAM_MEMBER_ROLES.join(", ")})`, + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + if (!(TEAM_MEMBER_ROLES as readonly string[]).includes(flags.role)) { + console.error( + `Error: Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`, + ); + process.exit(1); + } + + const auth = resolveAuth(flags); + try { + const member = await request(auth, "POST", "/v2/teams/member", { + team_id: flags.teamId, + member_email: flags.memberEmail, + member_role: flags.role, + }); + if (flags.json) { + printJSON(member); + return; + } + console.log(`${flags.memberEmail} added to team as '${flags.role}'.`); + console.log(); + printKeyValue(member as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/team/add_member.ts b/src/commands/team/add_member.ts deleted file mode 100644 index d0ad0bb..0000000 --- a/src/commands/team/add_member.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -enum Role { - admin = "admin", - dev = "dev", - finance = "finance", -} -export const addMemberCmd = new Command() - .name("add-member") - .description("Add a new member to a team") - .type("role", new cliffy.EnumType(Role)) - .option("--id ", "The id of your team", { required: true }) - .option( - "--member-email ", - "The email of a user you want to add.", - { - required: true, - }, - ) - .option("--role ", "The role for the new user", { - required: true, - }) - .example( - "Add new developer", - `upstash team add-member --id=f860e7e2-27b8-4166-90d5-ea41e90b4809 --member-email=bob@acme.com --role=${Role.dev}`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - if (!options.id) { - if (options.ci) { - throw new cliffy.ValidationError("id"); - } - const teams = await http.request< - { team_name: string; team_id: string }[] - >({ - method: "GET", - authorization, - path: ["v2", "teams"], - }); - options.id = await cliffy.Select.prompt({ - message: "Select a team", - options: teams.map(({ team_name, team_id }) => ({ - name: team_name, - value: team_id, - })), - }); - } - if (!options.memberEmail) { - if (options.ci) { - throw new cliffy.ValidationError("memberEmail"); - } - options.memberEmail = await cliffy.Input.prompt("Enter the user's email"); - } - if (!options.role) { - if (options.ci) { - throw new cliffy.ValidationError("role"); - } - options.role = (await cliffy.Select.prompt({ - message: "Select a role", - options: Object.entries(Role).map(([name, value]) => ({ - name, - value, - })), - })) as Role; - } - - const res = await http.request<{ - team_name: string; - member_email: string; - member_role: Role; - }>({ - method: "POST", - authorization, - path: ["v2", "teams", "member"], - body: { - team_id: options.id, - member_email: options.memberEmail, - member_role: options.role, - }, - }); - if (options.json) { - console.log(JSON.stringify(res, null, 2)); - return; - } - console.log( - cliffy.colors.brightGreen( - `${res.member_email} has been invited to join ${res.team_name} as ${res.member_role}`, - ), - ); - }); diff --git a/src/commands/team/create.ts b/src/commands/team/create.ts index a0a8cd4..9d54a4f 100644 --- a/src/commands/team/create.ts +++ b/src/commands/team/create.ts @@ -1,49 +1,42 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { Team } from "../../types.js"; -export const createCmd = new Command() - .name("create") - .description("Create a new team") - .option("-n --name=", "Name of the database", { - required: true, - }) - .option( - "--copy-credit-card=", - "Set true to copy the credit card information to the new team", - { default: false }, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + name: string; + copyCc?: boolean; +} - // if (!options.name) { - // if (options.ci) { - // throw new cliffy.ValidationError("name"); - // } - // options.name = await cliffy.Input.prompt("Set a name for your team"); - // } - - const body: Record = { - team_name: options.name, - copy_cc: options.copyCreditCard, - }; - - const team = await http.request({ - method: "POST", - authorization, - path: ["v2", "team"], - body, +export function registerTeamCreate(team: Command): void { + team + .command("create") + .description("Create a new team") + .requiredOption("--name ", "Team name") + .option("--copy-cc", "Copy existing credit card information to the team") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const t = await request(auth, "POST", "/v2/team", { + team_name: flags.name, + copy_cc: flags.copyCc ?? false, + }); + if (flags.json) { + printJSON(t); + return; + } + console.log(`Team '${t.team_name}' created.`); + console.log(); + printKeyValue(t as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify(team, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Team has been created")); - console.log(); - console.log( - cliffy.Table.from( - Object.entries(team).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - }); +} diff --git a/src/commands/team/delete.ts b/src/commands/team/delete.ts index 02e2f07..dcc722b 100644 --- a/src/commands/team/delete.ts +++ b/src/commands/team/delete.ts @@ -1,44 +1,44 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; -export const deleteCmd = new Command() - .name("delete") - .description("delete a team") - .option("--id ", "The uuid of your database") - .example("Delete", `upstash team delete f860e7e2-27b8-4166-90d5-ea41e90b4809`) - .action(async (options): Promise => { - const authorization = await parseAuth(options); +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const teams = await http.request< - // { team_name: string; team_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "teams"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a team to delete", - // options: teams.map(({ team_name, team_id }) => ({ - // name: team_name, - // value: team_id, - // })), - // }); - // } +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + dryRun?: boolean; +} - await http.request({ - method: "DELETE", - authorization, - path: ["v2", "team", options.id!], +export function registerTeamDelete(team: Command): void { + team + .command("delete ") + .description("Delete a team") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--dry-run", "Preview the action without executing it") + .action(async (teamId: string, flags: Flags) => { + if (flags.dryRun) { + const preview = { action: "delete", team_id: teamId, dry_run: true }; + if (flags.json) { + printJSON(preview); + return; + } + console.log(`Dry run: would delete team ${teamId}`); + return; + } + + const auth = resolveAuth(flags); + try { + await request(auth, "DELETE", `/v2/team/${teamId}`); + if (flags.json) { + printJSON({ deleted: true, team_id: teamId }); + return; + } + console.log(`Team ${teamId} deleted.`); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify({ ok: true }, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Team has been deleted")); - console.log(); - }); +} diff --git a/src/commands/team/index.ts b/src/commands/team/index.ts new file mode 100644 index 0000000..b51ffce --- /dev/null +++ b/src/commands/team/index.ts @@ -0,0 +1,18 @@ +import { Command } from "commander"; +import { registerTeamList } from "./list.js"; +import { registerTeamCreate } from "./create.js"; +import { registerTeamDelete } from "./delete.js"; +import { registerTeamMembers } from "./members.js"; +import { registerTeamAddMember } from "./add-member.js"; +import { registerTeamRemoveMember } from "./remove-member.js"; + +export function registerTeam(program: Command): void { + const team = program.command("team").description("Manage teams"); + + registerTeamList(team); + registerTeamCreate(team); + registerTeamDelete(team); + registerTeamMembers(team); + registerTeamAddMember(team); + registerTeamRemoveMember(team); +} diff --git a/src/commands/team/list.ts b/src/commands/team/list.ts index d28143c..5d56867 100644 --- a/src/commands/team/list.ts +++ b/src/commands/team/list.ts @@ -1,36 +1,40 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printTable, handleError } from "../../output.js"; +import type { Team } from "../../types.js"; -export const listCmd = new Command() - .name("list") - .description("list all your teams") - .example("List", "upstash team list") - .action(async (options): Promise => { - const authorization = await parseAuth(options); +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} - const teams = await http.request<{ team_name: string }[]>({ - method: "GET", - authorization, - path: ["v2", "teams"], +export function registerTeamList(team: Command): void { + team + .command("list") + .description("List all teams") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const teams = await request(auth, "GET", "/v2/teams"); + if (flags.json) { + printJSON(teams); + return; + } + if (teams.length === 0) { + console.log("No teams found."); + return; + } + printTable( + ["ID", "NAME", "COPY_CC"], + teams.map((t) => [t.team_id, t.team_name, t.copy_cc ? "yes" : "no"]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } }); - if (options.json) { - console.log(JSON.stringify(teams, null, 2)); - return; - } - - teams.forEach((team) => { - console.log(); - console.log(); - console.log( - cliffy.colors.underline(cliffy.colors.brightGreen(team.team_name)), - ); - console.log(); - console.log( - cliffy.Table.from( - Object.entries(team).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - }); - }); +} diff --git a/src/commands/team/list_members.ts b/src/commands/team/list_members.ts deleted file mode 100644 index 6872a8b..0000000 --- a/src/commands/team/list_members.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; - -export const listMembersCmd = new Command() - .name("list-members") - .description("List all members of a team") - .option("--id ", "The team id") - .example("List", "upstash team list-members") - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("teamID"); - // } - // const teams = await http.request< - // { team_name: string; team_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "teams"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a team to delete", - // options: teams.map(({ team_name, team_id }) => ({ - // name: team_name, - // value: team_id, - // })), - // }); - // } - const members = await http.request< - { database_name: string; database_id: string }[] - >({ - method: "GET", - authorization, - path: ["v2", "teams", options.id!], - }); - if (options.json) { - console.log(JSON.stringify(members, null, 2)); - return; - } - - members.forEach((member) => { - console.log( - cliffy.Table.from( - Object.entries(member).map(([k, v]) => [k.toString(), v.toString()]), - ).toString(), - ); - console.log(); - }); - console.log(); - console.log(); - }); diff --git a/src/commands/team/members.ts b/src/commands/team/members.ts new file mode 100644 index 0000000..6fc4836 --- /dev/null +++ b/src/commands/team/members.ts @@ -0,0 +1,40 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printTable, handleError } from "../../output.js"; +import type { TeamMember } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; +} + +export function registerTeamMembers(team: Command): void { + team + .command("members ") + .description("List all members of a team") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (teamId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const members = await request(auth, "GET", `/v2/teams/${teamId}`); + if (flags.json) { + printJSON(members); + return; + } + if (members.length === 0) { + console.log("No members found."); + return; + } + printTable( + ["EMAIL", "ROLE"], + members.map((m) => [m.member_email, m.member_role]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/team/mod.ts b/src/commands/team/mod.ts deleted file mode 100644 index 85714dc..0000000 --- a/src/commands/team/mod.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Command } from "../../util/command.ts"; -import { createCmd } from "./create.ts"; -import { listCmd } from "./list.ts"; -import { deleteCmd } from "./delete.ts"; -import { addMemberCmd } from "./add_member.ts"; -import { removeMemberCmd } from "./remove_member.ts"; -import { listMembersCmd } from "./list_members.ts"; - -export const teamCmd = new Command() - .description("Manage your teams and their members") - .globalOption("--json=[boolean:boolean]", "Return raw json response") - .command("create", createCmd) - .command("list", listCmd) - .command("delete", deleteCmd) - .command("add-member", addMemberCmd) - .command("remove-member", removeMemberCmd) - .command("list-members", listMembersCmd); - -teamCmd.reset().action(() => { - teamCmd.showHelp(); -}); diff --git a/src/commands/team/remove-member.ts b/src/commands/team/remove-member.ts new file mode 100644 index 0000000..5b08b12 --- /dev/null +++ b/src/commands/team/remove-member.ts @@ -0,0 +1,58 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + dryRun?: boolean; + teamId: string; + memberEmail: string; +} + +export function registerTeamRemoveMember(team: Command): void { + team + .command("remove-member") + .description("Remove a member from a team") + .requiredOption("--team-id ", "Team ID") + .requiredOption("--member-email ", "Email of the member to remove") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--dry-run", "Preview the action without executing it") + .action(async (flags: Flags) => { + if (flags.dryRun) { + const preview = { + action: "remove-member", + team_id: flags.teamId, + member_email: flags.memberEmail, + dry_run: true, + }; + if (flags.json) { + printJSON(preview); + return; + } + console.log( + `Dry run: would remove ${flags.memberEmail} from team ${flags.teamId}`, + ); + return; + } + + const auth = resolveAuth(flags); + try { + await request(auth, "DELETE", "/v2/teams/member", { + team_id: flags.teamId, + member_email: flags.memberEmail, + }); + if (flags.json) { + printJSON({ removed: true, team_id: flags.teamId, member_email: flags.memberEmail }); + return; + } + console.log(`${flags.memberEmail} removed from team ${flags.teamId}.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/team/remove_member.ts b/src/commands/team/remove_member.ts deleted file mode 100644 index 1ad93d6..0000000 --- a/src/commands/team/remove_member.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { cliffy } from "../../deps.ts"; -import { Command } from "../../util/command.ts"; -import { parseAuth } from "../../util/auth.ts"; -import { http } from "../../util/http.ts"; - -export const removeMemberCmd = new Command() - .name("remove-member") - .description("Remove a member from a team") - .option("--id=", "The uuid of the team") - .option("--email=", "The email of the member") - .example( - "Remove", - `upstash team remove-member f860e7e2-27b8-4166-90d5-ea41e90b4809 f860e7e2-27b8-4166-90d5-ea41e90b4809`, - ) - .action(async (options): Promise => { - const authorization = await parseAuth(options); - - // if (!options.id) { - // if (options.ci) { - // throw new cliffy.ValidationError("id"); - // } - // const teams = await http.request< - // { team_name: string; team_id: string }[] - // >({ - // method: "GET", - // authorization, - // path: ["v2", "teams"], - // }); - // options.id = await cliffy.Select.prompt({ - // message: "Select a team", - // options: teams.map(({ team_name, team_id }) => ({ - // name: team_name, - // value: team_id, - // })), - // }); - // } - // if (!options.email) { - // if (options.ci) { - // throw new cliffy.ValidationError("email"); - // } - // options.email = await cliffy.Input.prompt("Enter the user's email"); - // } - - const team = await http.request({ - method: "DELETE", - authorization, - path: ["v2", "teams", "member"], - body: { - team_id: options.id, - member_email: options.email, - }, - }); - if (options.json) { - console.log(JSON.stringify(team, null, 2)); - return; - } - console.log(cliffy.colors.brightGreen("Member has been removed")); - }); diff --git a/src/commands/vector/create.ts b/src/commands/vector/create.ts new file mode 100644 index 0000000..611560e --- /dev/null +++ b/src/commands/vector/create.ts @@ -0,0 +1,83 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { + VECTOR_REGIONS, + VECTOR_SIMILARITY_FUNCTIONS, + VECTOR_INDEX_TYPES, + VECTOR_EMBEDDING_MODELS, + VECTOR_SPARSE_MODELS, + VECTOR_PLANS, +} from "../../types.js"; +import type { VectorIndex } from "../../types.js"; + +interface Flags { + email?: string; + apiKey?: string; + json?: boolean; + name: string; + region: string; + similarityFunction: string; + dimensionCount: number; + type?: string; + embeddingModel?: string; + indexType?: string; + sparseEmbeddingModel?: string; +} + +export function registerVectorCreate(vector: Command): void { + vector + .command("create") + .description("Create a new vector index") + .requiredOption("--name ", "Index name") + .requiredOption( + "--region ", + `Region. Available: ${VECTOR_REGIONS.join(", ")}`, + ) + .requiredOption( + "--similarity-function ", + `Similarity function. Available: ${VECTOR_SIMILARITY_FUNCTIONS.join(", ")}`, + ) + .requiredOption("--dimension-count ", "Number of dimensions per vector", parseInt) + .option( + "--type ", + `Plan type. Available: ${VECTOR_PLANS.join(", ")}`, + ) + .option( + "--embedding-model ", + `Embedding model. Available: ${VECTOR_EMBEDDING_MODELS.join(", ")}`, + ) + .option( + "--index-type ", + `Index type. Available: ${VECTOR_INDEX_TYPES.join(", ")}`, + ) + .option( + "--sparse-embedding-model ", + `Sparse embedding model. Available: ${VECTOR_SPARSE_MODELS.join(", ")}`, + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request(auth, "POST", "/v2/vector/index", { + name: flags.name, + region: flags.region, + similarity_function: flags.similarityFunction, + dimension_count: flags.dimensionCount, + type: flags.type, + embedding_model: flags.embeddingModel, + index_type: flags.indexType, + sparse_embedding_model: flags.sparseEmbeddingModel, + }); + if (flags.json) { printJSON(idx); return; } + console.log(`Vector index '${idx.name}' created.`); + console.log(); + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/delete.ts b/src/commands/vector/delete.ts new file mode 100644 index 0000000..1b2869c --- /dev/null +++ b/src/commands/vector/delete.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; dryRun?: boolean } + +export function registerVectorDelete(vector: Command): void { + vector + .command("delete ") + .description("Delete a vector index") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .option("--dry-run", "Preview the action without executing it") + .action(async (indexId: string, flags: Flags) => { + if (flags.dryRun) { + const preview = { action: "delete", index_id: indexId, dry_run: true }; + if (flags.json) { printJSON(preview); return; } + console.log(`Dry run: would delete vector index ${indexId}`); + return; + } + const auth = resolveAuth(flags); + try { + await request(auth, "DELETE", `/v2/vector/index/${indexId}`); + if (flags.json) { printJSON({ deleted: true, index_id: indexId }); return; } + console.log(`Vector index ${indexId} deleted.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/get.ts b/src/commands/vector/get.ts new file mode 100644 index 0000000..14e20df --- /dev/null +++ b/src/commands/vector/get.ts @@ -0,0 +1,26 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { VectorIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerVectorGet(vector: Command): void { + vector + .command("get ") + .description("Get details of a vector index") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request(auth, "GET", `/v2/vector/index/${indexId}`); + if (flags.json) { printJSON(idx); return; } + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/index.ts b/src/commands/vector/index.ts new file mode 100644 index 0000000..fae8e43 --- /dev/null +++ b/src/commands/vector/index.ts @@ -0,0 +1,24 @@ +import { Command } from "commander"; +import { registerVectorList } from "./list.js"; +import { registerVectorCreate } from "./create.js"; +import { registerVectorGet } from "./get.js"; +import { registerVectorDelete } from "./delete.js"; +import { registerVectorRename } from "./rename.js"; +import { registerVectorResetPassword } from "./reset-password.js"; +import { registerVectorSetPlan } from "./set-plan.js"; +import { registerVectorTransfer } from "./transfer.js"; +import { registerVectorStats } from "./stats.js"; + +export function registerVector(program: Command): void { + const vector = program.command("vector").description("Manage Vector indexes"); + + registerVectorList(vector); + registerVectorCreate(vector); + registerVectorGet(vector); + registerVectorDelete(vector); + registerVectorRename(vector); + registerVectorResetPassword(vector); + registerVectorSetPlan(vector); + registerVectorTransfer(vector); + registerVectorStats(vector); +} diff --git a/src/commands/vector/list.ts b/src/commands/vector/list.ts new file mode 100644 index 0000000..be7a013 --- /dev/null +++ b/src/commands/vector/list.ts @@ -0,0 +1,37 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printTable, handleError } from "../../output.js"; +import type { VectorIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerVectorList(vector: Command): void { + vector + .command("list") + .description("List all vector indexes") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const indexes = await request(auth, "GET", "/v2/vector/index"); + if (flags.json) { printJSON(indexes); return; } + if (indexes.length === 0) { console.log("No vector indexes found."); return; } + printTable( + ["ID", "NAME", "REGION", "TYPE", "SIMILARITY", "DIMENSIONS"], + indexes.map((i) => [ + i.id, + i.name, + i.region, + i.type, + i.similarity_function, + String(i.dimension_count), + ]), + ); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/rename.ts b/src/commands/vector/rename.ts new file mode 100644 index 0000000..31e150d --- /dev/null +++ b/src/commands/vector/rename.ts @@ -0,0 +1,34 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { VectorIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; name: string } + +export function registerVectorRename(vector: Command): void { + vector + .command("rename ") + .description("Rename a vector index") + .requiredOption("--name ", "New index name") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request( + auth, + "POST", + `/v2/vector/index/${indexId}/rename`, + { name: flags.name }, + ); + if (flags.json) { printJSON(idx); return; } + console.log(`Index renamed to '${idx.name}'.`); + console.log(); + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/reset-password.ts b/src/commands/vector/reset-password.ts new file mode 100644 index 0000000..b9def72 --- /dev/null +++ b/src/commands/vector/reset-password.ts @@ -0,0 +1,32 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, printKeyValue, handleError } from "../../output.js"; +import type { VectorIndex } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean } + +export function registerVectorResetPassword(vector: Command): void { + vector + .command("reset-password ") + .description("Reset tokens for a vector index") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + const idx = await request( + auth, + "POST", + `/v2/vector/index/${indexId}/reset-password`, + ); + if (flags.json) { printJSON(idx); return; } + console.log("Tokens reset successfully."); + console.log(); + printKeyValue(idx as unknown as Record); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/set-plan.ts b/src/commands/vector/set-plan.ts new file mode 100644 index 0000000..304dd48 --- /dev/null +++ b/src/commands/vector/set-plan.ts @@ -0,0 +1,29 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; +import { VECTOR_PLANS } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; plan: string } + +export function registerVectorSetPlan(vector: Command): void { + vector + .command("set-plan ") + .description(`Change the plan of a vector index. Plans: ${VECTOR_PLANS.join(", ")}`) + .requiredOption("--plan ", `Target plan (${VECTOR_PLANS.join(", ")})`) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/vector/index/${indexId}/setplan`, { + target_plan: flags.plan, + }); + if (flags.json) { printJSON({ success: true, index_id: indexId, plan: flags.plan }); return; } + console.log(`Plan changed to '${flags.plan}'.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/stats.ts b/src/commands/vector/stats.ts new file mode 100644 index 0000000..4f8577c --- /dev/null +++ b/src/commands/vector/stats.ts @@ -0,0 +1,55 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; +import { STATS_PERIODS } from "../../types.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; period?: string } + +export function registerVectorStats(vector: Command): void { + vector + .command("stats") + .description("Get statistics across all vector indexes") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (flags: Flags) => { + const auth = resolveAuth(flags); + try { + const stats = await request>( + auth, + "GET", + "/v2/vector/index/stats", + ); + printJSON(stats); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); + + vector + .command("index-stats ") + .description("Get statistics for a specific vector index") + .option( + "--period ", + `Time period for aggregation. Available: ${STATS_PERIODS.join(", ")}`, + "1h", + ) + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + const qs = flags.period ? `?period=${flags.period}` : ""; + try { + const stats = await request>( + auth, + "GET", + `/v2/vector/index/${indexId}/stats${qs}`, + ); + printJSON(stats); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/commands/vector/transfer.ts b/src/commands/vector/transfer.ts new file mode 100644 index 0000000..d69b770 --- /dev/null +++ b/src/commands/vector/transfer.ts @@ -0,0 +1,31 @@ +import { Command } from "commander"; +import { resolveAuth } from "../../auth.js"; +import { request } from "../../client.js"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { email?: string; apiKey?: string; json?: boolean; targetAccount: string } + +export function registerVectorTransfer(vector: Command): void { + vector + .command("transfer ") + .description("Transfer a vector index to another team") + .requiredOption("--target-account ", "Target team ID") + .option("--email ", "Upstash email") + .option("--api-key ", "Upstash API key") + .option("--json", "Output as JSON") + .action(async (indexId: string, flags: Flags) => { + const auth = resolveAuth(flags); + try { + await request(auth, "POST", `/v2/vector/index/${indexId}/transfer`, { + target_account: flags.targetAccount, + }); + if (flags.json) { + printJSON({ success: true, index_id: indexId, target_account: flags.targetAccount }); + return; + } + console.log(`Index ${indexId} transferred to team ${flags.targetAccount}.`); + } catch (err) { + handleError(err, flags.json ?? false); + } + }); +} diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index fd80f2a..0000000 --- a/src/config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { path } from "./deps.ts"; -export type Config = { - email: string; - apiKey: string; -}; -const homeDir = Deno.env.get("HOME"); -const fileName = ".upstash.json"; -export const DEFAULT_CONFIG_PATH = homeDir - ? path.join(homeDir, fileName) - : fileName; - -export function loadConfig(path: string): Config | null { - try { - return JSON.parse(Deno.readTextFileSync(path)) as Config; - } catch { - return null; - } -} - -export function storeConfig(path: string, config: Config): void { - Deno.writeTextFileSync(path, JSON.stringify(config)); -} - -export function deleteConfig(path: string): void { - Deno.removeSync(path); -} diff --git a/src/deps.ts b/src/deps.ts deleted file mode 100644 index d3b06f8..0000000 --- a/src/deps.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as cliffy from "https://deno.land/x/cliffy@v0.24.0/mod.ts"; -export * as path from "https://deno.land/std@0.139.0/path/mod.ts"; -export * as base64 from "https://deno.land/std@0.139.0/encoding/base64.ts"; diff --git a/src/mod.ts b/src/mod.ts deleted file mode 100644 index e212575..0000000 --- a/src/mod.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { authCmd } from "./commands/auth/mod.ts"; -import { redisCmd } from "./commands/redis/mod.ts"; -import { teamCmd } from "./commands/team/mod.ts"; -import { Command } from "./util/command.ts"; -import { cliffy } from "./deps.ts"; -import { VERSION } from "./version.ts"; -import { DEFAULT_CONFIG_PATH } from "./config.ts"; -const cmd = new Command() - .name("upstash") - .version(VERSION) - .description("Official cli for Upstash products") - .globalEnv("UPSTASH_EMAIL=", "The email you use on upstash") - .globalEnv("UPSTASH_API_KEY=", "The api key from upstash") - // .globalEnv( - // "CI=", - // "Disable interactive prompts and throws an error instead", - // { hidden: true } - // ) - // .globalOption( - // "--non-interactive [boolean]", - // "Disable interactive prompts and throws an error instead", - // { hidden: true } - // ) - .globalOption("-c, --config=", "Path to .upstash.json file", { - default: DEFAULT_CONFIG_PATH, - }) - /** - * Nested commands don't seem to work as expected, or maybe I'm just not understanding them. - * The workaround is to cast as `Command` - */ - .command("auth", authCmd as unknown as Command) - .command("redis", redisCmd as unknown as Command) - .command("team", teamCmd as unknown as Command); -cmd.reset().action(() => { - cmd.showHelp(); -}); - -await cmd.parse(Deno.args).catch((err) => { - if (err instanceof cliffy.ValidationError) { - cmd.showHelp(); - console.error("Usage error: %s", err.message); - Deno.exit(err.exitCode); - } else { - console.error(`Error: ${err.message}`); - - Deno.exit(1); - } -}); diff --git a/src/output.ts b/src/output.ts new file mode 100644 index 0000000..dc501d7 --- /dev/null +++ b/src/output.ts @@ -0,0 +1,37 @@ +export function printJSON(data: unknown): void { + console.log(JSON.stringify(data, null, 2)); +} + +export function printTable(headers: string[], rows: string[][]): void { + const colWidths = headers.map((h, i) => + Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)), + ); + const formatRow = (row: string[]) => + row.map((val, i) => val.padEnd(colWidths[i]!)).join(" "); + console.log(formatRow(headers)); + for (const row of rows) { + console.log(formatRow(row)); + } +} + +export function printKeyValue(obj: Record): void { + const maxKeyLen = Math.max(...Object.keys(obj).map((k) => k.length)); + for (const [key, val] of Object.entries(obj)) { + const formatted = Array.isArray(val) + ? val.join(", ") + : val === null || val === undefined + ? "" + : String(val); + console.log(`${key.padEnd(maxKeyLen + 2)}${formatted}`); + } +} + +export function handleError(err: unknown, json: boolean): never { + const message = err instanceof Error ? err.message : String(err); + if (json) { + console.error(JSON.stringify({ error: message })); + } else { + console.error(`Error: ${message}`); + } + process.exit(1); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..40dc453 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,171 @@ +export const REGIONS = [ + // AWS + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "sa-east-1", + "ap-south-1", + "ap-northeast-1", + "ap-southeast-1", + "ap-southeast-2", + "af-south-1", + // GCP + "us-central1", + "us-east4", + "europe-west1", + "asia-northeast1", +] as const; + +export type Region = (typeof REGIONS)[number]; + +export interface Database { + database_id: string; + database_name: string; + password?: string; + endpoint?: string; + port?: number; + creation_time?: number; + state?: string; + tls?: boolean; + type?: string; + budget?: number; + primary_region?: string; + read_regions?: string[]; + eviction?: boolean; + auto_upgrade?: boolean; + consistent?: boolean; + daily_backup_enabled?: boolean; + region?: string; + db_max_clients?: number; + db_memory_threshold?: number; + db_disk_threshold?: number; + db_max_entry_size?: number; + db_max_request_size?: number; +} + +export interface Backup { + backup_id: string; + backup_name: string; + creation_time: number; + database_id?: string; +} + +export interface Team { + team_id: string; + team_name: string; + copy_cc?: boolean; +} + +export interface TeamMember { + team_id: string; + team_name: string; + member_email: string; + member_role: "owner" | "admin" | "dev" | "finance"; + copy_cc?: boolean; +} + +export const TEAM_MEMBER_ROLES = ["admin", "dev", "finance"] as const; +export type TeamMemberRole = (typeof TEAM_MEMBER_ROLES)[number]; + +// ── Vector ──────────────────────────────────────────────────────────────────── + +export const VECTOR_REGIONS = ["eu-west-1", "us-east-1", "us-central1"] as const; +export type VectorRegion = (typeof VECTOR_REGIONS)[number]; + +export const VECTOR_SIMILARITY_FUNCTIONS = ["COSINE", "EUCLIDEAN", "DOT_PRODUCT"] as const; +export const VECTOR_INDEX_TYPES = ["DENSE", "SPARSE", "HYBRID"] as const; +export const VECTOR_EMBEDDING_MODELS = [ + "BGE_SMALL_EN_V1_5", + "BGE_BASE_EN_V1_5", + "BGE_LARGE_EN_V1_5", + "BGE_M3", +] as const; +export const VECTOR_SPARSE_MODELS = ["BM25", "BGE_M3"] as const; +export const VECTOR_PLANS = ["free", "payg", "fixed"] as const; + +export interface VectorIndex { + id: string; + name: string; + region: string; + similarity_function: string; + dimension_count: number; + embedding_model?: string; + sparse_embedding_model?: string; + index_type?: string; + endpoint: string; + token: string; + read_only_token: string; + type: string; + creation_time?: number; + customer_id?: string; + max_vector_count?: number; + max_daily_queries?: number; + max_daily_updates?: number; + max_writes_per_second?: number; + max_query_per_second?: number; + reserved_price?: number; +} + +// ── Search ──────────────────────────────────────────────────────────────────── + +export const SEARCH_REGIONS = ["eu-west-1", "us-central1"] as const; +export type SearchRegion = (typeof SEARCH_REGIONS)[number]; + +export const SEARCH_PLANS = ["free", "payg", "fixed"] as const; + +export interface SearchIndex { + id: string; + name: string; + region: string; + type: string; + endpoint: string; + token: string; + read_only_token: string; + creation_time?: number; + customer_id?: string; + max_vector_count?: number; + max_daily_queries?: number; + max_daily_updates?: number; + max_writes_per_second?: number; + max_query_per_second?: number; + input_enrichment_enabled?: boolean; +} + +// ── QStash ──────────────────────────────────────────────────────────────────── + +export const QSTASH_PLANS = [ + "paid", + "qstash_fixed_1m", + "qstash_fixed_10m", + "qstash_fixed_100m", +] as const; + +export const STATS_PERIODS = ["1h", "3h", "12h", "1d", "3d", "7d", "30d"] as const; +export type StatsPeriod = (typeof STATS_PERIODS)[number]; + +export interface QStashUser { + id: string; + customer_id: string; + token: string; + password?: string; + active: boolean; + state: string; + type: string; + region: string; + reserved_type?: string; + reserved_price?: number; + budget?: number; + prod_pack_enabled?: boolean; + max_requests_per_day?: number; + max_requests_per_second?: number; + max_topics?: number; + max_schedules?: number; + max_queues?: number; + timeout?: number; + creation_time?: number; +} diff --git a/src/util/auth.ts b/src/util/auth.ts deleted file mode 100644 index 50596f8..0000000 --- a/src/util/auth.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { base64 } from "../deps.ts"; -import { loadConfig } from "../config.ts"; -/** - * Parse cli config and return a basic auth header string - */ -export async function parseAuth(options: { - upstashEmail?: string; - upstashApiKey?: string; - config: string; - ci?: boolean; - [key: string]: unknown; -}): Promise { - let email = options.upstashEmail; - let apiKey = options.upstashApiKey; - const config = loadConfig(options.config); - if (config?.email) { - email = config.email; - } - if (config?.apiKey) { - apiKey = config.apiKey; - } - - if (!email || !apiKey) { - throw new Error( - `Not authenticated, please run "upstash auth login" or specify your config file with "--config=/path/to/.upstash.json"`, - ); - } - - return await Promise.resolve( - `Basic ${base64.encode([email, apiKey].join(":"))}`, - ); -} diff --git a/src/util/command.ts b/src/util/command.ts deleted file mode 100644 index 6110ce4..0000000 --- a/src/util/command.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { cliffy } from "../deps.ts"; - -type GlobalConfig = { - upstashEmail?: string; - upstashApiKey?: string; - ci?: boolean; - json?: boolean; - config: string; -}; - -export class Command extends cliffy.Command {} diff --git a/src/util/http.ts b/src/util/http.ts deleted file mode 100644 index 776e16e..0000000 --- a/src/util/http.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type UpstashRequest = { - authorization: string; - method: "GET" | "POST" | "PUT" | "DELETE"; - path?: string[]; - /** - * Request body will be serialized to json - */ - body?: unknown; -}; - -type HttpClientConfig = { - baseUrl: string; -}; - -class HttpClient { - private readonly baseUrl: string; - - public constructor(config: HttpClientConfig) { - this.baseUrl = config.baseUrl.replace(/\/$/, ""); - } - - public async request(req: UpstashRequest): Promise { - if (!req.path) { - req.path = []; - } - - const url = [this.baseUrl, ...req.path].join("/"); - const init: RequestInit = { - method: req.method, - headers: { - "Content-Type": "application/json", - Authorization: req.authorization, - }, - }; - if (req.method !== "GET") { - init.body = JSON.stringify(req.body); - } - - // fetch is defined by isomorphic fetch - // eslint-disable-next-line no-undef - const res = await fetch(url, init); - if (!res.ok) { - throw new Error(await res.text()); - } - return (await res.json()) as TResponse; - } -} - -export const http = new HttpClient({ baseUrl: "https://api.upstash.com" }); diff --git a/src/version.ts b/src/version.ts deleted file mode 100644 index 3f64591..0000000 --- a/src/version.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This is set during build -export const VERSION = "development"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..df0f4fd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src"] +} From 3cc0be28cc044d03289e34e777d214b8123d70f7 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Mon, 30 Mar 2026 21:53:03 +0300 Subject: [PATCH 02/18] refactor: streamline CLI commands and remove deprecated authentication methods --- .../skills/upstash-cli/SKILL.md | 215 +++++++++--------- CLAUDE.md | 214 ++++++++--------- Makefile | 23 -- README.md | 199 ++++++++-------- img/banner.svg | 17 -- package.json | 6 +- src/auth.ts | 38 +--- src/cli.ts | 2 - src/commands/auth/index.ts | 48 ---- src/commands/qstash/disable-prodpack.ts | 15 +- src/commands/qstash/enable-prodpack.ts | 15 +- src/commands/qstash/get.ts | 17 +- src/commands/qstash/ipv4.ts | 12 +- src/commands/qstash/list.ts | 26 +-- src/commands/qstash/move-to-team.ts | 24 +- src/commands/qstash/rotate-token.ts | 23 +- src/commands/qstash/set-plan.ts | 17 +- src/commands/qstash/stats.ts | 22 +- src/commands/qstash/update-budget.ts | 21 +- src/commands/redis/backup/create.ts | 25 +- src/commands/redis/backup/delete.ts | 43 +--- src/commands/redis/backup/disable-daily.ts | 22 +- src/commands/redis/backup/enable-daily.ts | 22 +- src/commands/redis/backup/list.ts | 39 +--- src/commands/redis/backup/restore.ts | 31 +-- src/commands/redis/change-plan.ts | 31 +-- src/commands/redis/create.ts | 34 +-- src/commands/redis/delete.ts | 33 +-- src/commands/redis/disable-autoupgrade.ts | 22 +- src/commands/redis/disable-eviction.ts | 22 +- src/commands/redis/enable-autoupgrade.ts | 22 +- src/commands/redis/enable-eviction.ts | 22 +- src/commands/redis/enable-tls.ts | 22 +- src/commands/redis/exec.ts | 73 ++++++ src/commands/redis/get.ts | 27 +-- src/commands/redis/index.ts | 2 + src/commands/redis/list.ts | 33 +-- src/commands/redis/move-to-team.ts | 26 +-- src/commands/redis/rename.ts | 29 +-- src/commands/redis/reset-password.ts | 30 +-- src/commands/redis/stats.ts | 20 +- src/commands/redis/update-budget.ts | 25 +- src/commands/redis/update-regions.ts | 34 +-- src/commands/search/create.ts | 37 +-- src/commands/search/delete.ts | 19 +- src/commands/search/get.ts | 17 +- src/commands/search/list.ts | 16 +- src/commands/search/rename.ts | 24 +- src/commands/search/reset-password.ts | 23 +- src/commands/search/stats.ts | 27 +-- src/commands/search/transfer.ts | 22 +- src/commands/team/add-member.ts | 34 +-- src/commands/team/create.ts | 28 +-- src/commands/team/delete.ts | 31 +-- src/commands/team/list.ts | 26 +-- src/commands/team/members.ts | 31 +-- src/commands/team/remove-member.ts | 40 +--- src/commands/vector/create.ts | 65 +----- src/commands/vector/delete.ts | 19 +- src/commands/vector/get.ts | 17 +- src/commands/vector/list.ts | 23 +- src/commands/vector/rename.ts | 24 +- src/commands/vector/reset-password.ts | 23 +- src/commands/vector/set-plan.ts | 17 +- src/commands/vector/stats.ts | 33 +-- src/commands/vector/transfer.ts | 22 +- src/output.ts | 32 +-- src/types.ts | 2 + 68 files changed, 754 insertions(+), 1541 deletions(-) rename skill.md => .agents/skills/upstash-cli/SKILL.md (50%) delete mode 100644 Makefile delete mode 100644 img/banner.svg delete mode 100644 src/commands/auth/index.ts create mode 100644 src/commands/redis/exec.ts diff --git a/skill.md b/.agents/skills/upstash-cli/SKILL.md similarity index 50% rename from skill.md rename to .agents/skills/upstash-cli/SKILL.md index 1b1328d..7aeeb28 100644 --- a/skill.md +++ b/.agents/skills/upstash-cli/SKILL.md @@ -1,16 +1,18 @@ -# Upstash CLI — Agent Skill +--- +name: upstash-cli +description: Run the Upstash CLI (`upstash`) against the Upstash Developer API for Redis, Vector, Search, QStash, and teams. Use when listing or managing databases, backups, vector/search indexes, QStash instances, team members, stats, or any non-interactive Upstash automation with JSON output and terminal commands. +--- -The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and supports `--json` for structured output. +The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and always outputs JSON. ## Installation ```bash -# From the repo root -npm install -npm run build -npm link +npm i -g @upstash/cli ``` +From a clone (contributors / unreleased fixes): `npm install`, `npm run build`, then `node dist/cli.js …` or `npm link`. + ## Authentication Set environment variables (recommended for agents): @@ -20,11 +22,7 @@ export UPSTASH_EMAIL=you@example.com export UPSTASH_API_KEY=your_api_key ``` -Or save to `~/.upstash.json`: - -```bash -upstash auth login --email you@example.com --api-key your_api_key -``` +**Agents:** Prefer a **read-only** Developer API key in `UPSTASH_API_KEY` when you can. The API only returns what that key may access, and only actions permitted for read-only keys succeed; the rest fail at the API. ## Global Flags @@ -32,59 +30,69 @@ Every command accepts these flags: | Flag | Description | |------|-------------| -| `--email ` | Upstash email (overrides env/config) | -| `--api-key ` | Upstash API key (overrides env/config) | -| `--json` | Output structured JSON instead of human-readable text | +| `--email ` | Upstash email (overrides `UPSTASH_EMAIL`) | +| `--api-key ` | Upstash API key (overrides `UPSTASH_API_KEY`) | + +**Resource IDs** — use **`--flag `** (e.g. `--index-id `), same pattern as `--db-id ` for Redis. -## Output Formats +| Flag | Products | +|------|----------| +| `--db-id ` | Redis | +| `--index-id ` | Vector, Search | +| `--qstash-id ` | QStash | +| `--team-id ` | Team (`delete`, `members`; also `add-member` / `remove-member`) | -### Success with data (`--json`) +## Output Format + +All commands output JSON to stdout. Errors output `{ "error": "..." }` to stderr and exit with code 1. + +### Success with data ```json { "database_id": "...", "database_name": "mydb", "state": "active", ... } ``` -### Boolean operation success (`--json`) +### Boolean operation success ```json { "success": true, "database_id": "..." } ``` -### Delete success (`--json`) +### Delete success ```json { "deleted": true, "database_id": "..." } ``` -### Error (`--json`, exits with code 1) +### Error (exits with code 1) ```json { "error": "detailed error message" } ``` --- -## Auth Commands +## Redis Commands + +### Execute Commands ```bash -upstash auth login --email --api-key --json # Save credentials -upstash auth logout --json # Clear credentials -upstash auth whoami --json # Show current email +upstash redis exec --db-url --db-token --command "SET key value" +upstash redis exec --db-url --db-token --command "GET key" +upstash redis exec --db-url --db-token --command "HGETALL myhash" ``` ---- - -## Redis Commands +The `--db-url` and `--db-token` come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. ### Core CRUD ```bash -upstash redis list --json -upstash redis get --json -upstash redis get --hide-credentials --json # Omit password from output -upstash redis create --name --region --json -upstash redis create --name --region --read-regions --json -upstash redis delete --dry-run --json # Preview before deleting -upstash redis delete --json -upstash redis rename --name --json -upstash redis reset-password --json -upstash redis stats --json +upstash redis list +upstash redis get --db-id +upstash redis get --db-id --hide-credentials # Omit password from output +upstash redis create --name --region +upstash redis create --name --region --read-regions +upstash redis delete --db-id --dry-run # Preview before deleting +upstash redis delete --db-id +upstash redis rename --db-id --name +upstash redis reset-password --db-id +upstash redis stats --db-id ``` ### Available Redis Regions @@ -96,27 +104,27 @@ upstash redis stats --json ### Configuration ```bash -upstash redis enable-tls --json -upstash redis enable-eviction --json -upstash redis disable-eviction --json -upstash redis enable-autoupgrade --json -upstash redis disable-autoupgrade --json -upstash redis change-plan --plan --json # Plans: free, payg, pro, paid -upstash redis update-budget --budget --json # Monthly budget in cents -upstash redis update-regions --read-regions --json -upstash redis move-to-team --team-id --json +upstash redis enable-tls --db-id +upstash redis enable-eviction --db-id +upstash redis disable-eviction --db-id +upstash redis enable-autoupgrade --db-id +upstash redis disable-autoupgrade --db-id +upstash redis change-plan --db-id --plan # Plans: free, payg, pro, paid +upstash redis update-budget --db-id --budget # Monthly budget in cents +upstash redis update-regions --db-id --read-regions +upstash redis move-to-team --db-id --team-id ``` ### Backups ```bash -upstash redis backup list --json -upstash redis backup create --name --json -upstash redis backup delete --dry-run --json -upstash redis backup delete --json -upstash redis backup restore --backup-id --json -upstash redis backup enable-daily --json -upstash redis backup disable-daily --json +upstash redis backup list --db-id +upstash redis backup create --db-id --name +upstash redis backup delete --db-id --backup-id --dry-run +upstash redis backup delete --db-id --backup-id +upstash redis backup restore --db-id --backup-id +upstash redis backup enable-daily --db-id +upstash redis backup disable-daily --db-id ``` ### Redis Database Object Fields @@ -144,15 +152,15 @@ upstash redis backup disable-daily --json ## Team Commands ```bash -upstash team list --json -upstash team create --name --json -upstash team create --name --copy-cc --json # Copy credit card to team -upstash team delete --dry-run --json -upstash team delete --json -upstash team members --json # List team members -upstash team add-member --team-id --member-email --role --json -upstash team remove-member --team-id --member-email --dry-run --json -upstash team remove-member --team-id --member-email --json +upstash team list +upstash team create --name +upstash team create --name --copy-cc # Copy credit card to team +upstash team delete --team-id --dry-run +upstash team delete --team-id +upstash team members --team-id # List team members +upstash team add-member --team-id --member-email --role +upstash team remove-member --team-id --member-email --dry-run +upstash team remove-member --team-id --member-email ``` Member roles: `admin`, `dev`, `finance` @@ -178,20 +186,20 @@ Member roles: `admin`, `dev`, `finance` ## Vector Commands ```bash -upstash vector list --json -upstash vector get --json -upstash vector create --name --region --similarity-function --dimension-count --json -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg --json -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 --json -upstash vector delete --dry-run --json -upstash vector delete --json -upstash vector rename --name --json -upstash vector reset-password --json -upstash vector set-plan --plan --json # Plans: free, payg, fixed -upstash vector transfer --target-account --json -upstash vector stats --json # Aggregate stats across all indexes -upstash vector index-stats --json -upstash vector index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash vector list +upstash vector get --index-id +upstash vector create --name --region --similarity-function --dimension-count +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 +upstash vector delete --index-id --dry-run +upstash vector delete --index-id +upstash vector rename --index-id --name +upstash vector reset-password --index-id +upstash vector set-plan --index-id --plan # Plans: free, payg, fixed +upstash vector transfer --index-id --target-account +upstash vector stats # Aggregate stats across all indexes +upstash vector index-stats --index-id +upstash vector index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d ``` ### Available Vector Regions @@ -238,17 +246,17 @@ upstash vector index-stats --period --json # Periods: 1h, 3 ## Search Commands ```bash -upstash search list --json -upstash search get --json -upstash search create --name --region --type --json -upstash search delete --dry-run --json -upstash search delete --json -upstash search rename --name --json -upstash search reset-password --json -upstash search transfer --target-account --json -upstash search stats --json # Aggregate stats across all indexes -upstash search index-stats --json -upstash search index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash search list +upstash search get --index-id +upstash search create --name --region --type +upstash search delete --index-id --dry-run +upstash search delete --index-id +upstash search rename --index-id --name +upstash search reset-password --index-id +upstash search transfer --index-id --target-account +upstash search stats # Aggregate stats across all indexes +upstash search index-stats --index-id +upstash search index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d ``` ### Available Search Regions @@ -278,17 +286,17 @@ upstash search index-stats --period --json # Periods: 1h, ## QStash Commands ```bash -upstash qstash list --json # All instances; map `region` → `id` for other commands -upstash qstash get --json -upstash qstash rotate-token --json -upstash qstash set-plan --plan --json # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m -upstash qstash stats --json -upstash qstash stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -upstash qstash ipv4 --json # CIDR blocks for firewall allowlisting -upstash qstash move-to-team --qstash-id --target-team-id --json -upstash qstash update-budget --budget --json # 0 = no limit -upstash qstash enable-prodpack --json -upstash qstash disable-prodpack --json +upstash qstash list # All instances; map `region` → `id` for other commands +upstash qstash get --qstash-id +upstash qstash rotate-token --qstash-id +upstash qstash set-plan --qstash-id --plan # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m +upstash qstash stats --qstash-id +upstash qstash stats --qstash-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash qstash ipv4 # CIDR blocks for firewall allowlisting +upstash qstash move-to-team --qstash-id --target-team-id +upstash qstash update-budget --qstash-id --budget # 0 = no limit +upstash qstash enable-prodpack --qstash-id +upstash qstash disable-prodpack --qstash-id ``` ### QStashUser Object Fields @@ -316,14 +324,15 @@ upstash qstash disable-prodpack --json ## Tips for Agents -- Always use `--json` for reliable output parsing. +- All output is JSON — pipe through `jq` for field extraction. - Exit code `0` = success, `1` = error. - Use `--dry-run` before any `delete` or `remove-member` command to confirm the target. - Use `--hide-credentials` on `redis get` when the password is not needed. -- Pipe `--json` output through `jq` for field extraction: +- Run `upstash qstash list` first to discover which `id` maps to which `region`, then use those IDs for all other qstash commands. +- Field extraction examples: ```bash - upstash redis list --json | jq '.[].database_id' - upstash vector list --json | jq '.[] | {id, name, region}' - upstash qstash list --json | jq '.[] | {id, region}' - upstash team members --json | jq '.[].member_email' + upstash redis list | jq '.[].database_id' + upstash vector list | jq '.[] | {id, name, region}' + upstash qstash list | jq '.[] | {id, region}' + upstash team members --team-id | jq '.[].member_email' ``` diff --git a/CLAUDE.md b/CLAUDE.md index 54ad1a7..92b1b3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,31 +1,28 @@ # Upstash CLI — Agent Skill -The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and supports `--json` for structured output. +The same instructions are packaged for VS Code Agent Skills at `.agents/skills/upstash-cli/SKILL.md`. + +The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and always outputs JSON. ## Installation ```bash -# From the repo root -npm install -npm run build -npm link +npm i -g @upstash/cli ``` +From a clone: `npm install`, `npm run build`, then `node dist/cli.js …` or `npm link`. + ## Authentication -Set environment variables (recommended for agents): +Set environment variables before running any command: ```bash export UPSTASH_EMAIL=you@example.com export UPSTASH_API_KEY=your_api_key ``` -Or save to `~/.upstash.json`: - -```bash -upstash auth login --email you@example.com --api-key your_api_key -``` +**Agents:** You can use a **read-only** Developer API key in `UPSTASH_API_KEY`. The Developer API then only returns what that key is allowed to see, and only the actions allowed for that key will succeed; anything else fails at the API. ## Global Flags @@ -33,59 +30,69 @@ Every command accepts these flags: | Flag | Description | |------|-------------| -| `--email ` | Upstash email (overrides env/config) | -| `--api-key ` | Upstash API key (overrides env/config) | -| `--json` | Output structured JSON instead of human-readable text | +| `--email ` | Upstash email (overrides `UPSTASH_EMAIL`) | +| `--api-key ` | Upstash API key (overrides `UPSTASH_API_KEY`) | + +**Resource IDs** — scoped commands use `--db-id`, `--index-id`, `--qstash-id`, or `--team-id` followed by the placeholder **``** (e.g. `--index-id `), including in `--help` output. + +| Flag | Products | +|------|----------| +| `--db-id ` | Redis | +| `--index-id ` | Vector, Search | +| `--qstash-id ` | QStash | +| `--team-id ` | Team (`delete`, `members`; also `add-member` / `remove-member`) | -## Output Formats +## Output Format -### Success with data (`--json`) +All commands output JSON to stdout. Errors output `{ "error": "..." }` to stderr and exit with code 1. + +### Success with data ```json { "database_id": "...", "database_name": "mydb", "state": "active", ... } ``` -### Boolean operation success (`--json`) +### Boolean operation success ```json { "success": true, "database_id": "..." } ``` -### Delete success (`--json`) +### Delete success ```json { "deleted": true, "database_id": "..." } ``` -### Error (`--json`, exits with code 1) +### Error (exits with code 1) ```json { "error": "detailed error message" } ``` --- -## Auth Commands +## Redis Commands + +### Execute Commands ```bash -upstash auth login --email --api-key --json # Save credentials -upstash auth logout --json # Clear credentials -upstash auth whoami --json # Show current email +upstash redis exec --db-url --db-token --command "SET key value" +upstash redis exec --db-url --db-token --command "GET key" +upstash redis exec --db-url --db-token --command "HGETALL myhash" ``` ---- - -## Redis Commands +The `--db-url` and `--db-token` come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. ### Core CRUD ```bash -upstash redis list --json -upstash redis get --json -upstash redis get --hide-credentials --json # Omit password from output -upstash redis create --name --region --json -upstash redis create --name --region --read-regions --json -upstash redis delete --dry-run --json # Preview before deleting -upstash redis delete --json -upstash redis rename --name --json -upstash redis reset-password --json -upstash redis stats --json +upstash redis list +upstash redis get --db-id +upstash redis get --db-id --hide-credentials # Omit password from output +upstash redis create --name --region +upstash redis create --name --region --read-regions +upstash redis delete --db-id --dry-run # Preview before deleting +upstash redis delete --db-id +upstash redis rename --db-id --name +upstash redis reset-password --db-id +upstash redis stats --db-id ``` ### Available Redis Regions @@ -97,27 +104,27 @@ upstash redis stats --json ### Configuration ```bash -upstash redis enable-tls --json -upstash redis enable-eviction --json -upstash redis disable-eviction --json -upstash redis enable-autoupgrade --json -upstash redis disable-autoupgrade --json -upstash redis change-plan --plan --json # Plans: free, payg, pro, paid -upstash redis update-budget --budget --json # Monthly budget in cents -upstash redis update-regions --read-regions --json -upstash redis move-to-team --team-id --json +upstash redis enable-tls --db-id +upstash redis enable-eviction --db-id +upstash redis disable-eviction --db-id +upstash redis enable-autoupgrade --db-id +upstash redis disable-autoupgrade --db-id +upstash redis change-plan --db-id --plan # Plans: free, payg, pro, paid +upstash redis update-budget --db-id --budget # Monthly budget in cents +upstash redis update-regions --db-id --read-regions +upstash redis move-to-team --db-id --team-id ``` ### Backups ```bash -upstash redis backup list --json -upstash redis backup create --name --json -upstash redis backup delete --dry-run --json -upstash redis backup delete --json -upstash redis backup restore --backup-id --json -upstash redis backup enable-daily --json -upstash redis backup disable-daily --json +upstash redis backup list --db-id +upstash redis backup create --db-id --name +upstash redis backup delete --db-id --backup-id --dry-run +upstash redis backup delete --db-id --backup-id +upstash redis backup restore --db-id --backup-id +upstash redis backup enable-daily --db-id +upstash redis backup disable-daily --db-id ``` ### Redis Database Object Fields @@ -145,15 +152,15 @@ upstash redis backup disable-daily --json ## Team Commands ```bash -upstash team list --json -upstash team create --name --json -upstash team create --name --copy-cc --json # Copy credit card to team -upstash team delete --dry-run --json -upstash team delete --json -upstash team members --json # List team members -upstash team add-member --team-id --member-email --role --json -upstash team remove-member --team-id --member-email --dry-run --json -upstash team remove-member --team-id --member-email --json +upstash team list +upstash team create --name +upstash team create --name --copy-cc # Copy credit card to team +upstash team delete --team-id --dry-run +upstash team delete --team-id +upstash team members --team-id # List team members +upstash team add-member --team-id --member-email --role +upstash team remove-member --team-id --member-email --dry-run +upstash team remove-member --team-id --member-email ``` Member roles: `admin`, `dev`, `finance` @@ -179,20 +186,20 @@ Member roles: `admin`, `dev`, `finance` ## Vector Commands ```bash -upstash vector list --json -upstash vector get --json -upstash vector create --name --region --similarity-function --dimension-count --json -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg --json -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 --json -upstash vector delete --dry-run --json -upstash vector delete --json -upstash vector rename --name --json -upstash vector reset-password --json -upstash vector set-plan --plan --json # Plans: free, payg, fixed -upstash vector transfer --target-account --json -upstash vector stats --json # Aggregate stats across all indexes -upstash vector index-stats --json -upstash vector index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash vector list +upstash vector get --index-id +upstash vector create --name --region --similarity-function --dimension-count +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg +upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 +upstash vector delete --index-id --dry-run +upstash vector delete --index-id +upstash vector rename --index-id --name +upstash vector reset-password --index-id +upstash vector set-plan --index-id --plan # Plans: free, payg, fixed +upstash vector transfer --index-id --target-account +upstash vector stats # Aggregate stats across all indexes +upstash vector index-stats --index-id +upstash vector index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d ``` ### Available Vector Regions @@ -239,17 +246,17 @@ upstash vector index-stats --period --json # Periods: 1h, 3 ## Search Commands ```bash -upstash search list --json -upstash search get --json -upstash search create --name --region --type --json -upstash search delete --dry-run --json -upstash search delete --json -upstash search rename --name --json -upstash search reset-password --json -upstash search transfer --target-account --json -upstash search stats --json # Aggregate stats across all indexes -upstash search index-stats --json -upstash search index-stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash search list +upstash search get --index-id +upstash search create --name --region --type +upstash search delete --index-id --dry-run +upstash search delete --index-id +upstash search rename --index-id --name +upstash search reset-password --index-id +upstash search transfer --index-id --target-account +upstash search stats # Aggregate stats across all indexes +upstash search index-stats --index-id +upstash search index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d ``` ### Available Search Regions @@ -279,17 +286,17 @@ upstash search index-stats --period --json # Periods: 1h, ## QStash Commands ```bash -upstash qstash list --json # All instances; map `region` → `id` for other commands -upstash qstash get --json -upstash qstash rotate-token --json -upstash qstash set-plan --plan --json # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m -upstash qstash stats --json -upstash qstash stats --period --json # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -upstash qstash ipv4 --json # CIDR blocks for firewall allowlisting -upstash qstash move-to-team --qstash-id --target-team-id --json -upstash qstash update-budget --budget --json # 0 = no limit -upstash qstash enable-prodpack --json -upstash qstash disable-prodpack --json +upstash qstash list # All instances; map `region` → `id` for other commands +upstash qstash get --qstash-id +upstash qstash rotate-token --qstash-id +upstash qstash set-plan --qstash-id --plan # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m +upstash qstash stats --qstash-id +upstash qstash stats --qstash-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash qstash ipv4 # CIDR blocks for firewall allowlisting +upstash qstash move-to-team --qstash-id --target-team-id +upstash qstash update-budget --qstash-id --budget # 0 = no limit +upstash qstash enable-prodpack --qstash-id +upstash qstash disable-prodpack --qstash-id ``` ### QStashUser Object Fields @@ -317,14 +324,15 @@ upstash qstash disable-prodpack --json ## Tips for Agents -- Always use `--json` for reliable output parsing. +- All output is JSON — pipe through `jq` for field extraction. - Exit code `0` = success, `1` = error. - Use `--dry-run` before any `delete` or `remove-member` command to confirm the target. - Use `--hide-credentials` on `redis get` when the password is not needed. -- Pipe `--json` output through `jq` for field extraction: +- Run `upstash qstash list` first to discover which `id` maps to which `region`, then use those IDs for all other qstash commands. +- Field extraction examples: ```bash - upstash redis list --json | jq '.[].database_id' - upstash vector list --json | jq '.[] | {id, name, region}' - upstash qstash list --json | jq '.[] | {id, region}' - upstash team members --json | jq '.[].member_email' + upstash redis list | jq '.[].database_id' + upstash vector list | jq '.[] | {id, name, region}' + upstash qstash list | jq '.[] | {id, region}' + upstash team members --team-id | jq '.[].member_email' ``` diff --git a/Makefile b/Makefile deleted file mode 100644 index a9b26fb..0000000 --- a/Makefile +++ /dev/null @@ -1,23 +0,0 @@ - - -fmt: clean - deno fmt - deno lint - - -clean: - rm -rf dist - -build-node: fmt - deno run -A ./cmd/build.ts - - -build-bin: fmt - deno compile \ - --allow-env \ - --allow-read \ - --allow-write \ - --allow-net \ - --output=./bin/upstash\ - ./src/mod.ts - diff --git a/README.md b/README.md index 4cf37df..0c6d12f 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,34 @@ # Upstash CLI -Manage Upstash services from the terminal or automation via the [Upstash Developer API](https://docs.upstash.com/redis/howto/developerapi). Commands are non-interactive and support `--json` for structured output. - -![](./img/banner.svg) +Manage Upstash services from the terminal or automation via the [Upstash Developer API](https://docs.upstash.com/redis/howto/developerapi). Commands are non-interactive; successful output on stdout is always JSON (parse with `jq` or similar). Errors go to stderr as `{ "error": "..." }` with exit code 1. ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/upstash/cli) [![Downloads/week](https://img.shields.io/npm/dw/lstr.svg)](https://npmjs.org/package/@upstash/cli) -**Agent reference:** the same command catalog lives in [`CLAUDE.md`](./CLAUDE.md) and [`skill.md`](./skill.md). +**Agent reference:** [`CLAUDE.md`](./CLAUDE.md) (Cursor rules). Full CLI catalog for agents also lives at [`.agents/skills/upstash-cli/SKILL.md`](./.agents/skills/upstash-cli/SKILL.md). ## Installation -### npm (global) - ```bash npm i -g @upstash/cli ``` -The binary is `upstash` on your `PATH`. +The `upstash` binary is on your `PATH`. -### From this repository - -```bash -npm install -npm run build -npm link -``` - -Compiled binaries for Windows, Linux, and macOS (Intel and Apple Silicon) are on the [releases page](https://github.com/upstash/cli/releases/latest). +Prebuilt binaries (Windows, Linux, macOS Intel and Apple Silicon) are on [GitHub Releases](https://github.com/upstash/cli/releases/latest). ## Authentication -Recommended for CI and agents: +Set both variables before running commands: ```bash export UPSTASH_EMAIL=you@example.com export UPSTASH_API_KEY=your_api_key ``` -Or save credentials to `~/.upstash.json`: +Most commands also accept `--email` and `--api-key`, which override the environment for that invocation. -```bash -upstash auth login --email you@example.com --api-key your_api_key -``` +For agents, a **read-only** Developer API key is often enough: the API only returns what that key allows, and only those operations succeed—mutations fail at the API like in the console. See [how to get an API key](https://docs.upstash.com/redis/howto/developerapi#api-development). @@ -52,14 +38,14 @@ These flags are accepted on commands that call the API: | Flag | Description | |------|-------------| -| `--email ` | Upstash email (overrides env / config file) | -| `--api-key ` | Upstash API key (overrides env / config file) | -| `--json` | Print structured JSON instead of human-readable text | +| `--email ` | Upstash email (overrides `UPSTASH_EMAIL`) | +| `--api-key ` | Upstash API key (overrides `UPSTASH_API_KEY`) | + +Scoped commands use explicit resource flags with a shared placeholder: `--db-id `, `--index-id `, `--qstash-id `, `--team-id ` (see `--help` on each command). ## Top-level commands ```text -upstash auth # Credentials upstash redis # Redis databases upstash team # Teams and members upstash vector # Vector indexes @@ -72,36 +58,37 @@ upstash --help upstash redis --help ``` -## Output shapes (`--json`) +## Output format - **Resource payload:** e.g. `{ "database_id": "...", "state": "active", ... }` - **Boolean success:** `{ "success": true, ... }` - **Delete:** `{ "deleted": true, ... }` -- **Error (exit code 1):** `{ "error": "message" }` +- **Error (exit code 1):** `{ "error": "message" }` on stderr + +## Redis commands -## Auth commands +### Execute via REST (`redis exec` does not use the Developer API key) ```bash -upstash auth login --email --api-key --json -upstash auth logout --json -upstash auth whoami --json +upstash redis exec --db-url --db-token --command "SET key value" +upstash redis exec --db-url --db-token --command "GET key" ``` -## Redis commands +Use `endpoint` / `https://...` and `rest_token` from `upstash redis get --db-id `. ### Core ```bash -upstash redis list --json -upstash redis get --json -upstash redis get --hide-credentials --json -upstash redis create --name --region --json -upstash redis create --name --region --read-regions --json -upstash redis delete --dry-run --json -upstash redis delete --json -upstash redis rename --name --json -upstash redis reset-password --json -upstash redis stats --json +upstash redis list +upstash redis get --db-id +upstash redis get --db-id --hide-credentials +upstash redis create --name --region +upstash redis create --name --region --read-regions +upstash redis delete --db-id --dry-run +upstash redis delete --db-id +upstash redis rename --db-id --name +upstash redis reset-password --db-id +upstash redis stats --db-id ``` **Regions — AWS:** `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1` @@ -110,41 +97,41 @@ upstash redis stats --json ### Configuration ```bash -upstash redis enable-tls --json -upstash redis enable-eviction --json -upstash redis disable-eviction --json -upstash redis enable-autoupgrade --json -upstash redis disable-autoupgrade --json -upstash redis change-plan --plan --json # free, payg, pro, paid -upstash redis update-budget --budget --json -upstash redis update-regions --read-regions --json -upstash redis move-to-team --team-id --json +upstash redis enable-tls --db-id +upstash redis enable-eviction --db-id +upstash redis disable-eviction --db-id +upstash redis enable-autoupgrade --db-id +upstash redis disable-autoupgrade --db-id +upstash redis change-plan --db-id --plan # free, payg, pro, paid +upstash redis update-budget --db-id --budget +upstash redis update-regions --db-id --read-regions +upstash redis move-to-team --db-id --team-id ``` ### Backups ```bash -upstash redis backup list --json -upstash redis backup create --name --json -upstash redis backup delete --dry-run --json -upstash redis backup delete --json -upstash redis backup restore --backup-id --json -upstash redis backup enable-daily --json -upstash redis backup disable-daily --json +upstash redis backup list --db-id +upstash redis backup create --db-id --name +upstash redis backup delete --db-id --backup-id --dry-run +upstash redis backup delete --db-id --backup-id +upstash redis backup restore --db-id --backup-id +upstash redis backup enable-daily --db-id +upstash redis backup disable-daily --db-id ``` ## Team commands ```bash -upstash team list --json -upstash team create --name --json -upstash team create --name --copy-cc --json -upstash team delete --dry-run --json -upstash team delete --json -upstash team members --json -upstash team add-member --team-id --member-email --role --json -upstash team remove-member --team-id --member-email --dry-run --json -upstash team remove-member --team-id --member-email --json +upstash team list +upstash team create --name +upstash team create --name --copy-cc +upstash team delete --team-id --dry-run +upstash team delete --team-id +upstash team members --team-id +upstash team add-member --team-id --member-email --role +upstash team remove-member --team-id --member-email --dry-run +upstash team remove-member --team-id --member-email ``` Member roles: `admin`, `dev`, `finance`. @@ -152,18 +139,18 @@ Member roles: `admin`, `dev`, `finance`. ## Vector commands ```bash -upstash vector list --json -upstash vector get --json -upstash vector create --name --region --similarity-function --dimension-count --json -upstash vector delete --dry-run --json -upstash vector delete --json -upstash vector rename --name --json -upstash vector reset-password --json -upstash vector set-plan --plan --json # free, payg, fixed -upstash vector transfer --target-account --json -upstash vector stats --json -upstash vector index-stats --json -upstash vector index-stats --period --json # 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash vector list +upstash vector get --index-id +upstash vector create --name --region --similarity-function --dimension-count +upstash vector delete --index-id --dry-run +upstash vector delete --index-id +upstash vector rename --index-id --name +upstash vector reset-password --index-id +upstash vector set-plan --index-id --plan # free, payg, fixed +upstash vector transfer --index-id --target-account +upstash vector stats +upstash vector index-stats --index-id +upstash vector index-stats --index-id --period # 1h, 3h, 12h, 1d, 3d, 7d, 30d ``` **Regions:** `eu-west-1`, `us-east-1`, `us-central1` @@ -173,17 +160,17 @@ upstash vector index-stats --period --json # 1h, 3h, 12h, ## Search commands ```bash -upstash search list --json -upstash search get --json -upstash search create --name --region --type --json -upstash search delete --dry-run --json -upstash search delete --json -upstash search rename --name --json -upstash search reset-password --json -upstash search transfer --target-account --json -upstash search stats --json -upstash search index-stats --json -upstash search index-stats --period --json +upstash search list +upstash search get --index-id +upstash search create --name --region --type +upstash search delete --index-id --dry-run +upstash search delete --index-id +upstash search rename --index-id --name +upstash search reset-password --index-id +upstash search transfer --index-id --target-account +upstash search stats +upstash search index-stats --index-id +upstash search index-stats --index-id --period ``` **Regions:** `eu-west-1`, `us-central1` @@ -192,30 +179,32 @@ upstash search index-stats --period --json ## QStash commands ```bash -upstash qstash list --json # All instances; map region → id for other commands -upstash qstash get --json -upstash qstash rotate-token --json -upstash qstash set-plan --plan --json # paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m -upstash qstash stats --json -upstash qstash stats --period --json # 1h, 3h, 12h, 1d, 3d, 7d, 30d -upstash qstash ipv4 --json -upstash qstash move-to-team --qstash-id --target-team-id --json -upstash qstash update-budget --budget --json # 0 = no limit -upstash qstash enable-prodpack --json -upstash qstash disable-prodpack --json +upstash qstash list # All instances; map region → id for other commands +upstash qstash get --qstash-id +upstash qstash rotate-token --qstash-id +upstash qstash set-plan --qstash-id --plan # paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m +upstash qstash stats --qstash-id +upstash qstash stats --qstash-id --period # 1h, 3h, 12h, 1d, 3d, 7d, 30d +upstash qstash ipv4 +upstash qstash move-to-team --qstash-id --target-team-id +upstash qstash update-budget --qstash-id --budget # 0 = no limit +upstash qstash enable-prodpack --qstash-id +upstash qstash disable-prodpack --qstash-id ``` ## Examples ```bash -upstash redis list --json | jq '.[].database_id' -upstash vector list --json | jq '.[] | {id, name, region}' -upstash qstash list --json | jq '.[] | {id, region}' -upstash team members --json | jq '.[].member_email' +upstash redis list | jq '.[].database_id' +upstash vector list | jq '.[] | {id, name, region}' +upstash qstash list | jq '.[] | {id, region}' +upstash team members --team-id | jq '.[].member_email' ``` Use `--dry-run` before `delete` or `remove-member`. Use `--hide-credentials` on `redis get` when you do not need the password. ## Contributing +From a clone: `npm install` and `npm run build`, then run `node dist/cli.js …` or `npm link` to try your build as `upstash`. + Issues and improvements: open a ticket or talk to us on [Discord](https://discord.com/invite/w9SenAtbme). diff --git a/img/banner.svg b/img/banner.svg deleted file mode 100644 index f651d90..0000000 --- a/img/banner.svg +++ /dev/null @@ -1,17 +0,0 @@ -
\ No newline at end of file diff --git a/package.json b/package.json index fbfc12b..8f71d40 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,10 @@ "name": "@upstash/cli", "version": "1.0.0", "description": "Agent-friendly CLI for Upstash", + "repository": { + "type": "git", + "url": "https://github.com/upstash/cli.git" + }, "type": "module", "bin": { "upstash": "./dist/cli.js" @@ -10,7 +14,7 @@ "build": "tsc", "dev": "tsc --watch" }, - "keywords": ["upstash", "cli", "ai-agent", "redis"], + "keywords": ["upstash", "cli"], "author": "Upstash", "license": "MIT", "dependencies": { diff --git a/src/auth.ts b/src/auth.ts index 9f26864..84a58d2 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,47 +1,15 @@ -import { readFileSync, writeFileSync, existsSync } from "fs"; -import { homedir } from "os"; -import { join } from "path"; - -const CONFIG_PATH = join(homedir(), ".upstash.json"); - export interface Auth { email: string; apiKey: string; } -interface Config { - email?: string; - api_key?: string; -} - -function readConfig(): Config { - if (!existsSync(CONFIG_PATH)) return {}; - try { - return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as Config; - } catch { - return {}; - } -} - -export function saveAuth(email: string, apiKey: string): void { - const config: Config = { email, api_key: apiKey }; - writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 }); -} - -export function clearAuth(): void { - writeFileSync(CONFIG_PATH, JSON.stringify({}), { mode: 0o600 }); -} - export function resolveAuth(flags: { email?: string; apiKey?: string }): Auth { - const config = readConfig(); - const email = flags.email ?? process.env.UPSTASH_EMAIL ?? config.email; - const apiKey = flags.apiKey ?? process.env.UPSTASH_API_KEY ?? config.api_key; + const email = flags.email ?? process.env.UPSTASH_EMAIL; + const apiKey = flags.apiKey ?? process.env.UPSTASH_API_KEY; if (!email || !apiKey) { console.error( - "Error: Authentication required.\n" + - " Set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables, or\n" + - " run: upstash auth login --email --api-key ", + JSON.stringify({ error: "Authentication required. Set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables." }), ); process.exit(1); } diff --git a/src/cli.ts b/src/cli.ts index 5456d42..82acabf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node import { Command } from "commander"; -import { registerAuth } from "./commands/auth/index.js"; import { registerRedis } from "./commands/redis/index.js"; import { registerTeam } from "./commands/team/index.js"; import { registerVector } from "./commands/vector/index.js"; @@ -14,7 +13,6 @@ program .description("Agent-friendly CLI for Upstash") .version("1.0.0"); -registerAuth(program); registerRedis(program); registerTeam(program); registerVector(program); diff --git a/src/commands/auth/index.ts b/src/commands/auth/index.ts deleted file mode 100644 index 9a5369b..0000000 --- a/src/commands/auth/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Command } from "commander"; -import { saveAuth, clearAuth, resolveAuth } from "../../auth.js"; -import { printJSON } from "../../output.js"; - -export function registerAuth(program: Command): void { - const auth = program.command("auth").description("Manage authentication credentials"); - - auth - .command("login") - .description("Save credentials to ~/.upstash.json") - .requiredOption("--email ", "Upstash email address") - .requiredOption("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action((flags: { email: string; apiKey: string; json?: boolean }) => { - saveAuth(flags.email, flags.apiKey); - if (flags.json) { - printJSON({ success: true, email: flags.email }); - return; - } - console.log(`Logged in as ${flags.email}`); - }); - - auth - .command("logout") - .description("Clear saved credentials from ~/.upstash.json") - .option("--json", "Output as JSON") - .action((flags: { json?: boolean }) => { - clearAuth(); - if (flags.json) { - printJSON({ success: true }); - return; - } - console.log("Logged out."); - }); - - auth - .command("whoami") - .description("Show the currently authenticated email") - .option("--json", "Output as JSON") - .action((flags: { json?: boolean }) => { - const creds = resolveAuth({}); - if (flags.json) { - printJSON({ email: creds.email }); - return; - } - console.log(`Logged in as ${creds.email}`); - }); -} diff --git a/src/commands/qstash/disable-prodpack.ts b/src/commands/qstash/disable-prodpack.ts index 767a0c7..54c7766 100644 --- a/src/commands/qstash/disable-prodpack.ts +++ b/src/commands/qstash/disable-prodpack.ts @@ -3,23 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerQStashDisableProdpack(qstash: Command): void { qstash - .command("disable-prodpack ") + .command("disable-prodpack") .description("Disable the production pack for a QStash instance") + .requiredOption("--qstash-id ", "QStash instance ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/qstash/disable-prodpack/${qstashId}`); - if (flags.json) { printJSON({ success: true, qstash_id: qstashId }); return; } - console.log("Production pack disabled."); + const result = await request(auth, "POST", `/v2/qstash/disable-prodpack/${flags.qstashId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/enable-prodpack.ts b/src/commands/qstash/enable-prodpack.ts index 1aa323a..0b0e3b7 100644 --- a/src/commands/qstash/enable-prodpack.ts +++ b/src/commands/qstash/enable-prodpack.ts @@ -3,23 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerQStashEnableProdpack(qstash: Command): void { qstash - .command("enable-prodpack ") + .command("enable-prodpack") .description("Enable the production pack for a QStash instance") + .requiredOption("--qstash-id ", "QStash instance ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/qstash/enable-prodpack/${qstashId}`); - if (flags.json) { printJSON({ success: true, qstash_id: qstashId }); return; } - console.log("Production pack enabled."); + const result = await request(auth, "POST", `/v2/qstash/enable-prodpack/${flags.qstashId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/get.ts b/src/commands/qstash/get.ts index 3781d9d..839c47d 100644 --- a/src/commands/qstash/get.ts +++ b/src/commands/qstash/get.ts @@ -1,26 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { QStashUser } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerQStashGet(qstash: Command): void { qstash - .command("get ") + .command("get") .description("Get details of a QStash instance") + .requiredOption("--qstash-id ", "QStash instance ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const q = await request(auth, "GET", `/v2/qstash/user/${qstashId}`); - if (flags.json) { printJSON(q); return; } - printKeyValue(q as unknown as Record); + const q = await request(auth, "GET", `/v2/qstash/user/${flags.qstashId}`); + printJSON(q); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/ipv4.ts b/src/commands/qstash/ipv4.ts index b33a748..cf032bf 100644 --- a/src/commands/qstash/ipv4.ts +++ b/src/commands/qstash/ipv4.ts @@ -3,25 +3,19 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerQStashIpv4(qstash: Command): void { qstash .command("ipv4") .description("List IPv4 CIDR blocks used by QStash (for firewall allowlisting)") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const addresses = await request(auth, "GET", "/v2/qstash/ipv4"); - if (flags.json) { printJSON(addresses); return; } - for (const addr of addresses) { - console.log(addr); - } + printJSON(addresses); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/list.ts b/src/commands/qstash/list.ts index 3446c6b..f280175 100644 --- a/src/commands/qstash/list.ts +++ b/src/commands/qstash/list.ts @@ -1,40 +1,22 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printTable, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { QStashUser } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerQStashList(qstash: Command): void { qstash .command("list") .description("List all QStash instances (id and region per deployment)") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const users = await request(auth, "GET", "/v2/qstash/users"); - if (flags.json) { - printJSON(users); - return; - } - if (users.length === 0) { - console.log("No QStash instances found."); - return; - } - printTable( - ["ID", "REGION", "STATE", "TYPE"], - users.map((u) => [u.id, u.region ?? "", u.state ?? "", u.type ?? ""]), - ); + printJSON(users); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/move-to-team.ts b/src/commands/qstash/move-to-team.ts index 69110f5..993c3fa 100644 --- a/src/commands/qstash/move-to-team.ts +++ b/src/commands/qstash/move-to-team.ts @@ -3,14 +3,6 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - qstashId: string; - targetTeamId: string; -} - export function registerQStashMoveToTeam(qstash: Command): void { qstash .command("move-to-team") @@ -19,21 +11,13 @@ export function registerQStashMoveToTeam(qstash: Command): void { .requiredOption("--target-team-id ", "Target team ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; qstashId: string; targetTeamId: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", "/v2/qstash/move-to-team", { - qstash_id: flags.qstashId, - target_team_id: flags.targetTeamId, - }); - if (flags.json) { - printJSON({ success: true, qstash_id: flags.qstashId, target_team_id: flags.targetTeamId }); - return; - } - console.log(`QStash ${flags.qstashId} moved to team ${flags.targetTeamId}.`); + const result = await request(auth, "POST", "/v2/qstash/move-to-team", { qstash_id: flags.qstashId, target_team_id: flags.targetTeamId }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/rotate-token.ts b/src/commands/qstash/rotate-token.ts index 8f33eb4..2a7048d 100644 --- a/src/commands/qstash/rotate-token.ts +++ b/src/commands/qstash/rotate-token.ts @@ -1,32 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { QStashUser } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerQStashRotateToken(qstash: Command): void { qstash - .command("rotate-token ") + .command("rotate-token") .description("Reset the authentication token for a QStash instance") + .requiredOption("--qstash-id ", "QStash instance ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const q = await request( - auth, - "POST", - `/v2/qstash/rotate-token/${qstashId}`, - ); - if (flags.json) { printJSON(q); return; } - console.log("Token rotated successfully."); - console.log(); - printKeyValue(q as unknown as Record); + const q = await request(auth, "POST", `/v2/qstash/rotate-token/${flags.qstashId}`); + printJSON(q); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/set-plan.ts b/src/commands/qstash/set-plan.ts index 17cfe1b..e81f437 100644 --- a/src/commands/qstash/set-plan.ts +++ b/src/commands/qstash/set-plan.ts @@ -4,26 +4,21 @@ import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; import { QSTASH_PLANS } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; plan: string } - export function registerQStashSetPlan(qstash: Command): void { qstash - .command("set-plan ") + .command("set-plan") .description(`Change the plan for a QStash instance. Plans: ${QSTASH_PLANS.join(", ")}`) + .requiredOption("--qstash-id ", "QStash instance ID") .requiredOption("--plan ", `Target plan (${QSTASH_PLANS.join(", ")})`) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string; plan: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/qstash/set-plan/${qstashId}`, { - plan_name: flags.plan, - }); - if (flags.json) { printJSON({ success: true, qstash_id: qstashId, plan: flags.plan }); return; } - console.log(`Plan changed to '${flags.plan}'.`); + const result = await request(auth, "POST", `/v2/qstash/set-plan/${flags.qstashId}`, { plan_name: flags.plan }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/stats.ts b/src/commands/qstash/stats.ts index 64dd777..19e7b1b 100644 --- a/src/commands/qstash/stats.ts +++ b/src/commands/qstash/stats.ts @@ -4,32 +4,22 @@ import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; import { STATS_PERIODS } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; period?: string } - export function registerQStashStats(qstash: Command): void { qstash - .command("stats ") + .command("stats") .description("Get usage statistics for a QStash instance") - .option( - "--period ", - `Time period for aggregation. Available: ${STATS_PERIODS.join(", ")}`, - "1h", - ) + .requiredOption("--qstash-id ", "QStash instance ID") + .option("--period ", `Time period. Available: ${STATS_PERIODS.join(", ")}`, "1h") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); const qs = flags.period ? `?period=${flags.period}` : ""; try { - const stats = await request>( - auth, - "GET", - `/v2/qstash/stats/${qstashId}${qs}`, - ); + const stats = await request>(auth, "GET", `/v2/qstash/stats/${flags.qstashId}${qs}`); printJSON(stats); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/qstash/update-budget.ts b/src/commands/qstash/update-budget.ts index c4a9224..7e221f4 100644 --- a/src/commands/qstash/update-budget.ts +++ b/src/commands/qstash/update-budget.ts @@ -3,30 +3,21 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; budget: number } - export function registerQStashUpdateBudget(qstash: Command): void { qstash - .command("update-budget ") + .command("update-budget") .description("Update the monthly spend budget for a QStash instance (in dollars, 0 = no limit)") + .requiredOption("--qstash-id ", "QStash instance ID") .requiredOption("--budget ", "Monthly budget in dollars (0 = no limit)", parseInt) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (qstashId: string, flags: Flags) => { + .action(async (flags: { qstashId: string; email?: string; apiKey?: string; budget: number }) => { const auth = resolveAuth(flags); try { - await request(auth, "PATCH", `/v2/qstash/update-budget/${qstashId}`, { - budget: flags.budget, - }); - if (flags.json) { - printJSON({ success: true, qstash_id: qstashId, budget: flags.budget }); - return; - } - const label = flags.budget === 0 ? "no limit" : `$${flags.budget}/month`; - console.log(`Budget updated to ${label}.`); + const result = await request(auth, "PATCH", `/v2/qstash/update-budget/${flags.qstashId}`, { budget: flags.budget }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/backup/create.ts b/src/commands/redis/backup/create.ts index bf09e9a..0d87c05 100644 --- a/src/commands/redis/backup/create.ts +++ b/src/commands/redis/backup/create.ts @@ -3,34 +3,21 @@ import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; import { printJSON, handleError } from "../../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - name: string; -} - export function registerBackupCreate(backup: Command): void { backup - .command("create ") + .command("create") .description("Create a backup of a Redis database") + .requiredOption("--db-id ", "Database ID") .requiredOption("--name ", "Backup name") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; name: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/create-backup/${databaseId}`, { - name: flags.name, - }); - if (flags.json) { - printJSON({ success: true, database_id: databaseId, name: flags.name }); - return; - } - console.log(`Backup '${flags.name}' created.`); + const result = await request(auth, "POST", `/v2/redis/create-backup/${flags.dbId}`, { name: flags.name }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/backup/delete.ts b/src/commands/redis/backup/delete.ts index 0212b84..26a0f18 100644 --- a/src/commands/redis/backup/delete.ts +++ b/src/commands/redis/backup/delete.ts @@ -3,51 +3,26 @@ import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; import { printJSON, handleError } from "../../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - dryRun?: boolean; -} - export function registerBackupDelete(backup: Command): void { backup - .command("delete ") + .command("delete") .description("Delete a backup of a Redis database") + .requiredOption("--db-id ", "Database ID") + .requiredOption("--backup-id ", "Backup ID") + .option("--dry-run", "Preview the action without executing it") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .option("--dry-run", "Preview the action without executing it") - .action(async (databaseId: string, backupId: string, flags: Flags) => { + .action(async (flags: { dbId: string; backupId: string; dryRun?: boolean; email?: string; apiKey?: string }) => { if (flags.dryRun) { - const preview = { - action: "delete-backup", - database_id: databaseId, - backup_id: backupId, - dry_run: true, - }; - if (flags.json) { - printJSON(preview); - return; - } - console.log(`Dry run: would delete backup ${backupId} from database ${databaseId}`); + printJSON({ action: "delete-backup", database_id: flags.dbId, backup_id: flags.backupId, dry_run: true }); return; } - const auth = resolveAuth(flags); try { - await request( - auth, - "DELETE", - `/v2/redis/delete-backup/${databaseId}/${backupId}`, - ); - if (flags.json) { - printJSON({ deleted: true, backup_id: backupId }); - return; - } - console.log(`Backup ${backupId} deleted.`); + await request(auth, "DELETE", `/v2/redis/delete-backup/${flags.dbId}/${flags.backupId}`); + printJSON({ deleted: true, backup_id: flags.backupId }); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/backup/disable-daily.ts b/src/commands/redis/backup/disable-daily.ts index 6ab764a..291a946 100644 --- a/src/commands/redis/backup/disable-daily.ts +++ b/src/commands/redis/backup/disable-daily.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; import { printJSON, handleError } from "../../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerDisableDaily(backup: Command): void { backup - .command("disable-daily ") + .command("disable-daily") .description("Disable daily automatic backups for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/disable-dailybackup/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("Daily backup disabled."); + const result = await request(auth, "POST", `/v2/redis/disable-dailybackup/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/backup/enable-daily.ts b/src/commands/redis/backup/enable-daily.ts index feec3cf..bd1e332 100644 --- a/src/commands/redis/backup/enable-daily.ts +++ b/src/commands/redis/backup/enable-daily.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; import { printJSON, handleError } from "../../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerEnableDaily(backup: Command): void { backup - .command("enable-daily ") + .command("enable-daily") .description("Enable daily automatic backups for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/enable-dailybackup/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("Daily backup enabled."); + const result = await request(auth, "POST", `/v2/redis/enable-dailybackup/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/backup/list.ts b/src/commands/redis/backup/list.ts index 1d69695..9f4b226 100644 --- a/src/commands/redis/backup/list.ts +++ b/src/commands/redis/backup/list.ts @@ -1,48 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, printTable, handleError } from "../../../output.js"; +import { printJSON, handleError } from "../../../output.js"; import type { Backup } from "../../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerBackupList(backup: Command): void { backup - .command("list ") + .command("list") .description("List all backups for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const backups = await request( - auth, - "GET", - `/v2/redis/list-backup/${databaseId}`, - ); - if (flags.json) { - printJSON(backups); - return; - } - if (backups.length === 0) { - console.log("No backups found."); - return; - } - printTable( - ["BACKUP_ID", "NAME", "CREATED"], - backups.map((b) => [ - b.backup_id, - b.backup_name, - new Date(b.creation_time * 1000).toISOString(), - ]), - ); + const backups = await request(auth, "GET", `/v2/redis/list-backup/${flags.dbId}`); + printJSON(backups); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/backup/restore.ts b/src/commands/redis/backup/restore.ts index c423414..b2b277a 100644 --- a/src/commands/redis/backup/restore.ts +++ b/src/commands/redis/backup/restore.ts @@ -3,40 +3,21 @@ import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; import { printJSON, handleError } from "../../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - backupId: string; -} - export function registerBackupRestore(backup: Command): void { backup - .command("restore ") + .command("restore") .description("Restore a Redis database from a backup") + .requiredOption("--db-id ", "Database ID") .requiredOption("--backup-id ", "ID of the backup to restore from") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; backupId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/restore-backup/${databaseId}`, { - backup_id: flags.backupId, - }); - if (flags.json) { - printJSON({ - success: true, - database_id: databaseId, - backup_id: flags.backupId, - }); - return; - } - console.log( - `Restore started for database ${databaseId} from backup ${flags.backupId}.`, - ); + const result = await request(auth, "POST", `/v2/redis/restore-backup/${flags.dbId}`, { backup_id: flags.backupId }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/change-plan.ts b/src/commands/redis/change-plan.ts index 7c51923..72eef03 100644 --- a/src/commands/redis/change-plan.ts +++ b/src/commands/redis/change-plan.ts @@ -3,36 +3,21 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -const PLANS = ["free", "payg", "pro", "paid"]; - -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - plan: string; -} - export function registerChangePlan(redis: Command): void { redis - .command("change-plan ") - .description(`Change the pricing plan of a Redis database. Plans: ${PLANS.join(", ")}`) - .requiredOption("--plan ", `Plan type (${PLANS.join(", ")})`) + .command("change-plan") + .description("Change the pricing plan of a Redis database. Plans: free, payg, pro, paid") + .requiredOption("--db-id ", "Database ID") + .requiredOption("--plan ", "Plan type (free, payg, pro, paid)") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; plan: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/change-plan/${databaseId}`, { - plan: flags.plan, - }); - if (flags.json) { - printJSON({ success: true, database_id: databaseId, plan: flags.plan }); - return; - } - console.log(`Plan changed to '${flags.plan}'.`); + const result = await request(auth, "POST", `/v2/redis/change-plan/${flags.dbId}`, { plan: flags.plan }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/create.ts b/src/commands/redis/create.ts index a55b47e..bf9aea0 100644 --- a/src/commands/redis/create.ts +++ b/src/commands/redis/create.ts @@ -1,40 +1,24 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import { REGIONS } from "../../types.js"; import type { Database } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - name: string; - region: string; - readRegions?: string[]; -} - export function registerCreate(redis: Command): void { redis .command("create") .description("Create a new Redis database") .requiredOption("--name ", "Database name") - .requiredOption( - "--region ", - `Primary region. Available: ${REGIONS.join(", ")}`, - ) + .requiredOption("--region ", `Primary region. Available: ${REGIONS.join(", ")}`) .option("--read-regions ", "Read replica regions (space-separated)") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; readRegions?: string[] }) => { if (!(REGIONS as readonly string[]).includes(flags.region)) { - console.error( - `Error: Invalid region '${flags.region}'.\nAvailable: ${REGIONS.join(", ")}`, - ); + console.error(JSON.stringify({ error: `Invalid region '${flags.region}'. Available: ${REGIONS.join(", ")}` })); process.exit(1); } - const auth = resolveAuth(flags); try { const db = await request(auth, "POST", "/v2/redis/database", { @@ -43,15 +27,9 @@ export function registerCreate(redis: Command): void { primary_region: flags.region, read_regions: flags.readRegions, }); - if (flags.json) { - printJSON(db); - return; - } - console.log(`Database '${db.database_name}' created.`); - console.log(); - printKeyValue(db as unknown as Record); + printJSON(db); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/delete.ts b/src/commands/redis/delete.ts index ba84fa1..3e3f5b0 100644 --- a/src/commands/redis/delete.ts +++ b/src/commands/redis/delete.ts @@ -3,42 +3,25 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - dryRun?: boolean; -} - export function registerDelete(redis: Command): void { redis - .command("delete ") + .command("delete") .description("Delete a Redis database") + .requiredOption("--db-id ", "Database ID") + .option("--dry-run", "Preview the action without executing it") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .option("--dry-run", "Preview the action without executing it") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; dryRun?: boolean; email?: string; apiKey?: string }) => { if (flags.dryRun) { - const preview = { action: "delete", database_id: databaseId, dry_run: true }; - if (flags.json) { - printJSON(preview); - return; - } - console.log(`Dry run: would delete database ${databaseId}`); + printJSON({ action: "delete", database_id: flags.dbId, dry_run: true }); return; } - const auth = resolveAuth(flags); try { - await request(auth, "DELETE", `/v2/redis/database/${databaseId}`); - if (flags.json) { - printJSON({ deleted: true, database_id: databaseId }); - return; - } - console.log(`Database ${databaseId} deleted.`); + await request(auth, "DELETE", `/v2/redis/database/${flags.dbId}`); + printJSON({ deleted: true, database_id: flags.dbId }); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/disable-autoupgrade.ts b/src/commands/redis/disable-autoupgrade.ts index 8ca8bd5..1f233d0 100644 --- a/src/commands/redis/disable-autoupgrade.ts +++ b/src/commands/redis/disable-autoupgrade.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerDisableAutoupgrade(redis: Command): void { redis - .command("disable-autoupgrade ") + .command("disable-autoupgrade") .description("Disable automatic version upgrades for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/disable-autoupgrade/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("Auto-upgrade disabled."); + const result = await request(auth, "POST", `/v2/redis/disable-autoupgrade/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/disable-eviction.ts b/src/commands/redis/disable-eviction.ts index 273a765..6c00554 100644 --- a/src/commands/redis/disable-eviction.ts +++ b/src/commands/redis/disable-eviction.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerDisableEviction(redis: Command): void { redis - .command("disable-eviction ") + .command("disable-eviction") .description("Disable key eviction for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/disable-eviction/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("Eviction disabled."); + const result = await request(auth, "POST", `/v2/redis/disable-eviction/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/enable-autoupgrade.ts b/src/commands/redis/enable-autoupgrade.ts index 60fb7d0..da3cff7 100644 --- a/src/commands/redis/enable-autoupgrade.ts +++ b/src/commands/redis/enable-autoupgrade.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerEnableAutoupgrade(redis: Command): void { redis - .command("enable-autoupgrade ") + .command("enable-autoupgrade") .description("Enable automatic version upgrades for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/enable-autoupgrade/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("Auto-upgrade enabled."); + const result = await request(auth, "POST", `/v2/redis/enable-autoupgrade/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/enable-eviction.ts b/src/commands/redis/enable-eviction.ts index 1851a64..2aaef31 100644 --- a/src/commands/redis/enable-eviction.ts +++ b/src/commands/redis/enable-eviction.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerEnableEviction(redis: Command): void { redis - .command("enable-eviction ") + .command("enable-eviction") .description("Enable key eviction for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/enable-eviction/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("Eviction enabled."); + const result = await request(auth, "POST", `/v2/redis/enable-eviction/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/enable-tls.ts b/src/commands/redis/enable-tls.ts index bf179c5..3d7c675 100644 --- a/src/commands/redis/enable-tls.ts +++ b/src/commands/redis/enable-tls.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerEnableTls(redis: Command): void { redis - .command("enable-tls ") + .command("enable-tls") .description("Enable TLS for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/enable-tls/${databaseId}`); - if (flags.json) { - printJSON({ success: true, database_id: databaseId }); - return; - } - console.log("TLS enabled."); + const result = await request(auth, "POST", `/v2/redis/enable-tls/${flags.dbId}`); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/exec.ts b/src/commands/redis/exec.ts new file mode 100644 index 0000000..99889b9 --- /dev/null +++ b/src/commands/redis/exec.ts @@ -0,0 +1,73 @@ +import { Command } from "commander"; +import { printJSON, handleError } from "../../output.js"; + +interface Flags { + dbUrl: string; + dbToken: string; + command: string; +} + +export function registerExec(redis: Command): void { + redis + .command("exec") + .description("Execute a Redis command against a database via the REST API") + .requiredOption("--db-url ", "Database REST URL (e.g. https://xxx.upstash.io)") + .requiredOption("--db-token ", "Database REST token") + .requiredOption("--command ", 'Redis command to run (e.g. "SET key value")') + .action(async (flags: Flags) => { + const args = parseCommand(flags.command); + if (args.length === 0) { + handleError(new Error("Empty command")); + } + + try { + const url = flags.dbUrl.replace(/\/$/, ""); + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${flags.dbToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(args), + }); + + const data = await response.json() as { result?: unknown; error?: string }; + + if (data.error) { + console.error(JSON.stringify({ error: data.error })); + process.exit(1); + } + + printJSON({ result: data.result }); + } catch (err) { + handleError(err); + } + }); +} + +function parseCommand(input: string): string[] { + const args: string[] = []; + let current = ""; + let inSingle = false; + let inDouble = false; + + for (let i = 0; i < input.length; i++) { + const ch = input[i]!; + + if (ch === "'" && !inDouble) { + inSingle = !inSingle; + } else if (ch === '"' && !inSingle) { + inDouble = !inDouble; + } else if (ch === " " && !inSingle && !inDouble) { + if (current.length > 0) { + args.push(current); + current = ""; + } + } else { + current += ch; + } + } + + if (current.length > 0) args.push(current); + return args; +} diff --git a/src/commands/redis/get.ts b/src/commands/redis/get.ts index b0b75c6..ee407ba 100644 --- a/src/commands/redis/get.ts +++ b/src/commands/redis/get.ts @@ -1,36 +1,25 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { Database } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - hideCredentials?: boolean; -} - export function registerGet(redis: Command): void { redis - .command("get ") + .command("get") .description("Get details of a Redis database") + .requiredOption("--db-id ", "Database ID") + .option("--hide-credentials", "Omit password from output") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .option("--hide-credentials", "Omit password from output") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; hideCredentials?: boolean; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); const qs = flags.hideCredentials ? "?credentials=hide" : ""; try { - const db = await request(auth, "GET", `/v2/redis/database/${databaseId}${qs}`); - if (flags.json) { - printJSON(db); - return; - } - printKeyValue(db as unknown as Record); + const db = await request(auth, "GET", `/v2/redis/database/${flags.dbId}${qs}`); + printJSON(db); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/index.ts b/src/commands/redis/index.ts index ff95988..c339b4a 100644 --- a/src/commands/redis/index.ts +++ b/src/commands/redis/index.ts @@ -16,6 +16,7 @@ import { registerUpdateBudget } from "./update-budget.js"; import { registerUpdateRegions } from "./update-regions.js"; import { registerMoveToTeam } from "./move-to-team.js"; import { registerBackup } from "./backup/index.js"; +import { registerExec } from "./exec.js"; export function registerRedis(program: Command): void { const redis = program.command("redis").description("Manage Redis databases"); @@ -37,4 +38,5 @@ export function registerRedis(program: Command): void { registerUpdateRegions(redis); registerMoveToTeam(redis); registerBackup(redis); + registerExec(redis); } diff --git a/src/commands/redis/list.ts b/src/commands/redis/list.ts index 60a3a8a..2a6c994 100644 --- a/src/commands/redis/list.ts +++ b/src/commands/redis/list.ts @@ -1,47 +1,22 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printTable, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { Database } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerList(redis: Command): void { redis .command("list") .description("List all Redis databases") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const dbs = await request(auth, "GET", "/v2/redis/databases"); - if (flags.json) { - printJSON(dbs); - return; - } - if (dbs.length === 0) { - console.log("No databases found."); - return; - } - printTable( - ["ID", "NAME", "STATE", "REGION", "TYPE", "TLS"], - dbs.map((db) => [ - db.database_id, - db.database_name, - db.state ?? "", - db.primary_region ?? db.region ?? "", - db.type ?? "", - db.tls ? "yes" : "no", - ]), - ); + printJSON(dbs); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/move-to-team.ts b/src/commands/redis/move-to-team.ts index e64a0c8..d6fdfdb 100644 --- a/src/commands/redis/move-to-team.ts +++ b/src/commands/redis/move-to-team.ts @@ -3,35 +3,21 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - teamId: string; -} - export function registerMoveToTeam(redis: Command): void { redis - .command("move-to-team ") + .command("move-to-team") .description("Move a Redis database to a team account") + .requiredOption("--db-id ", "Database ID") .requiredOption("--team-id ", "Target team ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; teamId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/move-to-team`, { - database_id: databaseId, - team_id: flags.teamId, - }); - if (flags.json) { - printJSON({ success: true, database_id: databaseId, team_id: flags.teamId }); - return; - } - console.log(`Database ${databaseId} moved to team ${flags.teamId}.`); + const result = await request(auth, "POST", `/v2/redis/move-to-team`, { database_id: flags.dbId, team_id: flags.teamId }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/rename.ts b/src/commands/redis/rename.ts index 9b2dddd..63a94e0 100644 --- a/src/commands/redis/rename.ts +++ b/src/commands/redis/rename.ts @@ -1,39 +1,24 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { Database } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - name: string; -} - export function registerRename(redis: Command): void { redis - .command("rename ") + .command("rename") .description("Rename a Redis database") + .requiredOption("--db-id ", "Database ID") .requiredOption("--name ", "New database name") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; name: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const db = await request(auth, "POST", `/v2/redis/rename/${databaseId}`, { - name: flags.name, - }); - if (flags.json) { - printJSON(db); - return; - } - console.log(`Database renamed to '${db.database_name}'.`); - console.log(); - printKeyValue(db as unknown as Record); + const db = await request(auth, "POST", `/v2/redis/rename/${flags.dbId}`, { name: flags.name }); + printJSON(db); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/reset-password.ts b/src/commands/redis/reset-password.ts index ddb9cb0..158c7a1 100644 --- a/src/commands/redis/reset-password.ts +++ b/src/commands/redis/reset-password.ts @@ -1,39 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { Database } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerResetPassword(redis: Command): void { redis - .command("reset-password ") + .command("reset-password") .description("Reset the password of a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const db = await request( - auth, - "POST", - `/v2/redis/reset-password/${databaseId}`, - ); - if (flags.json) { - printJSON(db); - return; - } - console.log("Password reset successfully."); - console.log(); - printKeyValue(db as unknown as Record); + const db = await request(auth, "POST", `/v2/redis/reset-password/${flags.dbId}`); + printJSON(db); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/stats.ts b/src/commands/redis/stats.ts index 7c3c151..6db3f48 100644 --- a/src/commands/redis/stats.ts +++ b/src/commands/redis/stats.ts @@ -3,30 +3,20 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerStats(redis: Command): void { redis - .command("stats ") + .command("stats") .description("Get usage statistics for a Redis database") + .requiredOption("--db-id ", "Database ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const stats = await request>( - auth, - "GET", - `/v2/redis/stats/${databaseId}`, - ); + const stats = await request>(auth, "GET", `/v2/redis/stats/${flags.dbId}`); printJSON(stats); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/update-budget.ts b/src/commands/redis/update-budget.ts index e2b44e5..87ffe94 100644 --- a/src/commands/redis/update-budget.ts +++ b/src/commands/redis/update-budget.ts @@ -3,34 +3,21 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - budget: number; -} - export function registerUpdateBudget(redis: Command): void { redis - .command("update-budget ") + .command("update-budget") .description("Update the monthly spend budget for a Redis database (in cents)") + .requiredOption("--db-id ", "Database ID") .requiredOption("--budget ", "Monthly budget in cents", parseInt) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; budget: number; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "PATCH", `/v2/redis/update-budget/${databaseId}`, { - budget: flags.budget, - }); - if (flags.json) { - printJSON({ success: true, database_id: databaseId, budget: flags.budget }); - return; - } - console.log(`Budget updated to ${flags.budget} cents/month.`); + const result = await request(auth, "PATCH", `/v2/redis/update-budget/${flags.dbId}`, { budget: flags.budget }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/redis/update-regions.ts b/src/commands/redis/update-regions.ts index 20fd5a0..e726bec 100644 --- a/src/commands/redis/update-regions.ts +++ b/src/commands/redis/update-regions.ts @@ -4,41 +4,21 @@ import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; import { REGIONS } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - readRegions: string[]; -} - export function registerUpdateRegions(redis: Command): void { redis - .command("update-regions ") + .command("update-regions") .description("Update read replica regions for a Redis database") - .requiredOption( - "--read-regions ", - `Read replica regions (space-separated). Available: ${REGIONS.join(", ")}`, - ) + .requiredOption("--db-id ", "Database ID") + .requiredOption("--read-regions ", `Read replica regions. Available: ${REGIONS.join(", ")}`) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (databaseId: string, flags: Flags) => { + .action(async (flags: { dbId: string; readRegions: string[]; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/redis/update-regions/${databaseId}`, { - read_regions: flags.readRegions, - }); - if (flags.json) { - printJSON({ - success: true, - database_id: databaseId, - read_regions: flags.readRegions, - }); - return; - } - console.log(`Read regions updated: ${flags.readRegions.join(", ")}`); + const result = await request(auth, "POST", `/v2/redis/update-regions/${flags.dbId}`, { read_regions: flags.readRegions }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/create.ts b/src/commands/search/create.ts index 829fe4f..f58c290 100644 --- a/src/commands/search/create.ts +++ b/src/commands/search/create.ts @@ -1,49 +1,26 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import { SEARCH_REGIONS, SEARCH_PLANS } from "../../types.js"; import type { SearchIndex } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - name: string; - region: string; - type: string; -} - export function registerSearchCreate(search: Command): void { search .command("create") .description("Create a new search index") .requiredOption("--name ", "Index name") - .requiredOption( - "--region ", - `Region. Available: ${SEARCH_REGIONS.join(", ")}`, - ) - .requiredOption( - "--type ", - `Plan type. Available: ${SEARCH_PLANS.join(", ")}`, - ) + .requiredOption("--region ", `Region. Available: ${SEARCH_REGIONS.join(", ")}`) + .requiredOption("--type ", `Plan type. Available: ${SEARCH_PLANS.join(", ")}`) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; type: string }) => { const auth = resolveAuth(flags); try { - const idx = await request(auth, "POST", "/v2/search", { - name: flags.name, - region: flags.region, - type: flags.type, - }); - if (flags.json) { printJSON(idx); return; } - console.log(`Search index '${idx.name}' created.`); - console.log(); - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "POST", "/v2/search", { name: flags.name, region: flags.region, type: flags.type }); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/delete.ts b/src/commands/search/delete.ts index 2010f0d..f79ea64 100644 --- a/src/commands/search/delete.ts +++ b/src/commands/search/delete.ts @@ -3,30 +3,25 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; dryRun?: boolean } - export function registerSearchDelete(search: Command): void { search - .command("delete ") + .command("delete") .description("Delete a search index") + .requiredOption("--index-id ", "Search index ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") .option("--dry-run", "Preview the action without executing it") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; dryRun?: boolean }) => { if (flags.dryRun) { - const preview = { action: "delete", index_id: indexId, dry_run: true }; - if (flags.json) { printJSON(preview); return; } - console.log(`Dry run: would delete search index ${indexId}`); + printJSON({ action: "delete", index_id: flags.indexId, dry_run: true }); return; } const auth = resolveAuth(flags); try { - await request(auth, "DELETE", `/v2/search/${indexId}`); - if (flags.json) { printJSON({ deleted: true, index_id: indexId }); return; } - console.log(`Search index ${indexId} deleted.`); + await request(auth, "DELETE", `/v2/search/${flags.indexId}`); + printJSON({ deleted: true, index_id: flags.indexId }); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/get.ts b/src/commands/search/get.ts index 2683586..371b698 100644 --- a/src/commands/search/get.ts +++ b/src/commands/search/get.ts @@ -1,26 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { SearchIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerSearchGet(search: Command): void { search - .command("get ") + .command("get") .description("Get details of a search index") + .requiredOption("--index-id ", "Search index ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const idx = await request(auth, "GET", `/v2/search/${indexId}`); - if (flags.json) { printJSON(idx); return; } - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "GET", `/v2/search/${flags.indexId}`); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/list.ts b/src/commands/search/list.ts index 3393f76..9ef477d 100644 --- a/src/commands/search/list.ts +++ b/src/commands/search/list.ts @@ -1,30 +1,22 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printTable, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { SearchIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerSearchList(search: Command): void { search .command("list") .description("List all search indexes") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const indexes = await request(auth, "GET", "/v2/search"); - if (flags.json) { printJSON(indexes); return; } - if (indexes.length === 0) { console.log("No search indexes found."); return; } - printTable( - ["ID", "NAME", "REGION", "TYPE", "ENDPOINT"], - indexes.map((i) => [i.id, i.name, i.region, i.type, i.endpoint]), - ); + printJSON(indexes); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/rename.ts b/src/commands/search/rename.ts index 44fac98..9499a6c 100644 --- a/src/commands/search/rename.ts +++ b/src/commands/search/rename.ts @@ -1,34 +1,24 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { SearchIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; name: string } - export function registerSearchRename(search: Command): void { search - .command("rename ") + .command("rename") .description("Rename a search index") + .requiredOption("--index-id ", "Search index ID") .requiredOption("--name ", "New index name") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; name: string }) => { const auth = resolveAuth(flags); try { - const idx = await request( - auth, - "POST", - `/v2/search/${indexId}/rename`, - { name: flags.name }, - ); - if (flags.json) { printJSON(idx); return; } - console.log(`Index renamed to '${idx.name}'.`); - console.log(); - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/rename`, { name: flags.name }); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/reset-password.ts b/src/commands/search/reset-password.ts index 685b528..1a9ce60 100644 --- a/src/commands/search/reset-password.ts +++ b/src/commands/search/reset-password.ts @@ -1,32 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { SearchIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerSearchResetPassword(search: Command): void { search - .command("reset-password ") + .command("reset-password") .description("Reset tokens for a search index") + .requiredOption("--index-id ", "Search index ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const idx = await request( - auth, - "POST", - `/v2/search/${indexId}/reset-password`, - ); - if (flags.json) { printJSON(idx); return; } - console.log("Tokens reset successfully."); - console.log(); - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/reset-password`); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/stats.ts b/src/commands/search/stats.ts index c934e24..bc7de0b 100644 --- a/src/commands/search/stats.ts +++ b/src/commands/search/stats.ts @@ -4,48 +4,37 @@ import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; import { STATS_PERIODS } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; period?: string } - export function registerSearchStats(search: Command): void { search .command("stats") .description("Get statistics across all search indexes") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const stats = await request>(auth, "GET", "/v2/search/stats"); printJSON(stats); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); search - .command("index-stats ") + .command("index-stats") .description("Get statistics for a specific search index") - .option( - "--period ", - `Time period for aggregation. Available: ${STATS_PERIODS.join(", ")}`, - "1h", - ) + .requiredOption("--index-id ", "Search index ID") + .option("--period ", `Time period. Available: ${STATS_PERIODS.join(", ")}`, "1h") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); const qs = flags.period ? `?period=${flags.period}` : ""; try { - const stats = await request>( - auth, - "GET", - `/v2/search/${indexId}/stats${qs}`, - ); + const stats = await request>(auth, "GET", `/v2/search/${flags.indexId}/stats${qs}`); printJSON(stats); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/search/transfer.ts b/src/commands/search/transfer.ts index 004a896..7df0667 100644 --- a/src/commands/search/transfer.ts +++ b/src/commands/search/transfer.ts @@ -3,29 +3,21 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; targetAccount: string } - export function registerSearchTransfer(search: Command): void { search - .command("transfer ") + .command("transfer") .description("Transfer a search index to another team") - .requiredOption("--target-account ", "Target team ID") + .requiredOption("--index-id ", "Search index ID") + .requiredOption("--target-account ", "Target team ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; targetAccount: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/search/${indexId}/transfer`, { - target_account: flags.targetAccount, - }); - if (flags.json) { - printJSON({ success: true, index_id: indexId, target_account: flags.targetAccount }); - return; - } - console.log(`Index ${indexId} transferred to team ${flags.targetAccount}.`); + const result = await request(auth, "POST", `/v2/search/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/team/add-member.ts b/src/commands/team/add-member.ts index 22e4e5d..8828487 100644 --- a/src/commands/team/add-member.ts +++ b/src/commands/team/add-member.ts @@ -1,40 +1,24 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import { TEAM_MEMBER_ROLES } from "../../types.js"; import type { TeamMember } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - teamId: string; - memberEmail: string; - role: string; -} - export function registerTeamAddMember(team: Command): void { team .command("add-member") .description("Add a member to a team") .requiredOption("--team-id ", "Team ID") .requiredOption("--member-email ", "Email of the member to add") - .requiredOption( - "--role ", - `Member role (${TEAM_MEMBER_ROLES.join(", ")})`, - ) + .requiredOption("--role ", `Member role (${TEAM_MEMBER_ROLES.join(", ")})`) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; teamId: string; memberEmail: string; role: string }) => { if (!(TEAM_MEMBER_ROLES as readonly string[]).includes(flags.role)) { - console.error( - `Error: Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`, - ); + console.error(JSON.stringify({ error: `Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}` })); process.exit(1); } - const auth = resolveAuth(flags); try { const member = await request(auth, "POST", "/v2/teams/member", { @@ -42,15 +26,9 @@ export function registerTeamAddMember(team: Command): void { member_email: flags.memberEmail, member_role: flags.role, }); - if (flags.json) { - printJSON(member); - return; - } - console.log(`${flags.memberEmail} added to team as '${flags.role}'.`); - console.log(); - printKeyValue(member as unknown as Record); + printJSON(member); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/team/create.ts b/src/commands/team/create.ts index 9d54a4f..3f8ef81 100644 --- a/src/commands/team/create.ts +++ b/src/commands/team/create.ts @@ -1,17 +1,9 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { Team } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - name: string; - copyCc?: boolean; -} - export function registerTeamCreate(team: Command): void { team .command("create") @@ -20,23 +12,13 @@ export function registerTeamCreate(team: Command): void { .option("--copy-cc", "Copy existing credit card information to the team") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; name: string; copyCc?: boolean }) => { const auth = resolveAuth(flags); try { - const t = await request(auth, "POST", "/v2/team", { - team_name: flags.name, - copy_cc: flags.copyCc ?? false, - }); - if (flags.json) { - printJSON(t); - return; - } - console.log(`Team '${t.team_name}' created.`); - console.log(); - printKeyValue(t as unknown as Record); + const t = await request(auth, "POST", "/v2/team", { team_name: flags.name, copy_cc: flags.copyCc ?? false }); + printJSON(t); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/team/delete.ts b/src/commands/team/delete.ts index dcc722b..3188cee 100644 --- a/src/commands/team/delete.ts +++ b/src/commands/team/delete.ts @@ -3,42 +3,25 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - dryRun?: boolean; -} - export function registerTeamDelete(team: Command): void { team - .command("delete ") + .command("delete") .description("Delete a team") + .requiredOption("--team-id ", "Team ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") .option("--dry-run", "Preview the action without executing it") - .action(async (teamId: string, flags: Flags) => { + .action(async (flags: { teamId: string; email?: string; apiKey?: string; dryRun?: boolean }) => { if (flags.dryRun) { - const preview = { action: "delete", team_id: teamId, dry_run: true }; - if (flags.json) { - printJSON(preview); - return; - } - console.log(`Dry run: would delete team ${teamId}`); + printJSON({ action: "delete", team_id: flags.teamId, dry_run: true }); return; } - const auth = resolveAuth(flags); try { - await request(auth, "DELETE", `/v2/team/${teamId}`); - if (flags.json) { - printJSON({ deleted: true, team_id: teamId }); - return; - } - console.log(`Team ${teamId} deleted.`); + await request(auth, "DELETE", `/v2/team/${flags.teamId}`); + printJSON({ deleted: true, team_id: flags.teamId }); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/team/list.ts b/src/commands/team/list.ts index 5d56867..617598c 100644 --- a/src/commands/team/list.ts +++ b/src/commands/team/list.ts @@ -1,40 +1,22 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printTable, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { Team } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerTeamList(team: Command): void { team .command("list") .description("List all teams") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const teams = await request(auth, "GET", "/v2/teams"); - if (flags.json) { - printJSON(teams); - return; - } - if (teams.length === 0) { - console.log("No teams found."); - return; - } - printTable( - ["ID", "NAME", "COPY_CC"], - teams.map((t) => [t.team_id, t.team_name, t.copy_cc ? "yes" : "no"]), - ); + printJSON(teams); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/team/members.ts b/src/commands/team/members.ts index 6fc4836..4541ed7 100644 --- a/src/commands/team/members.ts +++ b/src/commands/team/members.ts @@ -1,40 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printTable, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { TeamMember } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; -} - export function registerTeamMembers(team: Command): void { team - .command("members ") + .command("members") .description("List all members of a team") + .requiredOption("--team-id ", "Team ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (teamId: string, flags: Flags) => { + .action(async (flags: { teamId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const members = await request(auth, "GET", `/v2/teams/${teamId}`); - if (flags.json) { - printJSON(members); - return; - } - if (members.length === 0) { - console.log("No members found."); - return; - } - printTable( - ["EMAIL", "ROLE"], - members.map((m) => [m.member_email, m.member_role]), - ); + const members = await request(auth, "GET", `/v2/teams/${flags.teamId}`); + printJSON(members); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/team/remove-member.ts b/src/commands/team/remove-member.ts index 5b08b12..f1b0b2c 100644 --- a/src/commands/team/remove-member.ts +++ b/src/commands/team/remove-member.ts @@ -3,15 +3,6 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - dryRun?: boolean; - teamId: string; - memberEmail: string; -} - export function registerTeamRemoveMember(team: Command): void { team .command("remove-member") @@ -20,39 +11,18 @@ export function registerTeamRemoveMember(team: Command): void { .requiredOption("--member-email ", "Email of the member to remove") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") .option("--dry-run", "Preview the action without executing it") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; dryRun?: boolean; teamId: string; memberEmail: string }) => { if (flags.dryRun) { - const preview = { - action: "remove-member", - team_id: flags.teamId, - member_email: flags.memberEmail, - dry_run: true, - }; - if (flags.json) { - printJSON(preview); - return; - } - console.log( - `Dry run: would remove ${flags.memberEmail} from team ${flags.teamId}`, - ); + printJSON({ action: "remove-member", team_id: flags.teamId, member_email: flags.memberEmail, dry_run: true }); return; } - const auth = resolveAuth(flags); try { - await request(auth, "DELETE", "/v2/teams/member", { - team_id: flags.teamId, - member_email: flags.memberEmail, - }); - if (flags.json) { - printJSON({ removed: true, team_id: flags.teamId, member_email: flags.memberEmail }); - return; - } - console.log(`${flags.memberEmail} removed from team ${flags.teamId}.`); + await request(auth, "DELETE", "/v2/teams/member", { team_id: flags.teamId, member_email: flags.memberEmail }); + printJSON({ removed: true, team_id: flags.teamId, member_email: flags.memberEmail }); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/create.ts b/src/commands/vector/create.ts index 611560e..78483a3 100644 --- a/src/commands/vector/create.ts +++ b/src/commands/vector/create.ts @@ -1,65 +1,25 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; -import { - VECTOR_REGIONS, - VECTOR_SIMILARITY_FUNCTIONS, - VECTOR_INDEX_TYPES, - VECTOR_EMBEDDING_MODELS, - VECTOR_SPARSE_MODELS, - VECTOR_PLANS, -} from "../../types.js"; +import { printJSON, handleError } from "../../output.js"; +import { VECTOR_REGIONS, VECTOR_SIMILARITY_FUNCTIONS, VECTOR_INDEX_TYPES, VECTOR_EMBEDDING_MODELS, VECTOR_SPARSE_MODELS, VECTOR_PLANS } from "../../types.js"; import type { VectorIndex } from "../../types.js"; -interface Flags { - email?: string; - apiKey?: string; - json?: boolean; - name: string; - region: string; - similarityFunction: string; - dimensionCount: number; - type?: string; - embeddingModel?: string; - indexType?: string; - sparseEmbeddingModel?: string; -} - export function registerVectorCreate(vector: Command): void { vector .command("create") .description("Create a new vector index") .requiredOption("--name ", "Index name") - .requiredOption( - "--region ", - `Region. Available: ${VECTOR_REGIONS.join(", ")}`, - ) - .requiredOption( - "--similarity-function ", - `Similarity function. Available: ${VECTOR_SIMILARITY_FUNCTIONS.join(", ")}`, - ) + .requiredOption("--region ", `Region. Available: ${VECTOR_REGIONS.join(", ")}`) + .requiredOption("--similarity-function ", `Similarity function. Available: ${VECTOR_SIMILARITY_FUNCTIONS.join(", ")}`) .requiredOption("--dimension-count ", "Number of dimensions per vector", parseInt) - .option( - "--type ", - `Plan type. Available: ${VECTOR_PLANS.join(", ")}`, - ) - .option( - "--embedding-model ", - `Embedding model. Available: ${VECTOR_EMBEDDING_MODELS.join(", ")}`, - ) - .option( - "--index-type ", - `Index type. Available: ${VECTOR_INDEX_TYPES.join(", ")}`, - ) - .option( - "--sparse-embedding-model ", - `Sparse embedding model. Available: ${VECTOR_SPARSE_MODELS.join(", ")}`, - ) + .option("--type ", `Plan type. Available: ${VECTOR_PLANS.join(", ")}`) + .option("--embedding-model ", `Embedding model. Available: ${VECTOR_EMBEDDING_MODELS.join(", ")}`) + .option("--index-type ", `Index type. Available: ${VECTOR_INDEX_TYPES.join(", ")}`) + .option("--sparse-embedding-model ", `Sparse embedding model. Available: ${VECTOR_SPARSE_MODELS.join(", ")}`) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; similarityFunction: string; dimensionCount: number; type?: string; embeddingModel?: string; indexType?: string; sparseEmbeddingModel?: string }) => { const auth = resolveAuth(flags); try { const idx = await request(auth, "POST", "/v2/vector/index", { @@ -72,12 +32,9 @@ export function registerVectorCreate(vector: Command): void { index_type: flags.indexType, sparse_embedding_model: flags.sparseEmbeddingModel, }); - if (flags.json) { printJSON(idx); return; } - console.log(`Vector index '${idx.name}' created.`); - console.log(); - printKeyValue(idx as unknown as Record); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/delete.ts b/src/commands/vector/delete.ts index 1b2869c..1026562 100644 --- a/src/commands/vector/delete.ts +++ b/src/commands/vector/delete.ts @@ -3,30 +3,25 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; dryRun?: boolean } - export function registerVectorDelete(vector: Command): void { vector - .command("delete ") + .command("delete") .description("Delete a vector index") + .requiredOption("--index-id ", "Vector index ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") .option("--dry-run", "Preview the action without executing it") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; dryRun?: boolean }) => { if (flags.dryRun) { - const preview = { action: "delete", index_id: indexId, dry_run: true }; - if (flags.json) { printJSON(preview); return; } - console.log(`Dry run: would delete vector index ${indexId}`); + printJSON({ action: "delete", index_id: flags.indexId, dry_run: true }); return; } const auth = resolveAuth(flags); try { - await request(auth, "DELETE", `/v2/vector/index/${indexId}`); - if (flags.json) { printJSON({ deleted: true, index_id: indexId }); return; } - console.log(`Vector index ${indexId} deleted.`); + await request(auth, "DELETE", `/v2/vector/index/${flags.indexId}`); + printJSON({ deleted: true, index_id: flags.indexId }); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/get.ts b/src/commands/vector/get.ts index 14e20df..1486c5a 100644 --- a/src/commands/vector/get.ts +++ b/src/commands/vector/get.ts @@ -1,26 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { VectorIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerVectorGet(vector: Command): void { vector - .command("get ") + .command("get") .description("Get details of a vector index") + .requiredOption("--index-id ", "Vector index ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const idx = await request(auth, "GET", `/v2/vector/index/${indexId}`); - if (flags.json) { printJSON(idx); return; } - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "GET", `/v2/vector/index/${flags.indexId}`); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/list.ts b/src/commands/vector/list.ts index be7a013..666bf3d 100644 --- a/src/commands/vector/list.ts +++ b/src/commands/vector/list.ts @@ -1,37 +1,22 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printTable, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { VectorIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerVectorList(vector: Command): void { vector .command("list") .description("List all vector indexes") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { const indexes = await request(auth, "GET", "/v2/vector/index"); - if (flags.json) { printJSON(indexes); return; } - if (indexes.length === 0) { console.log("No vector indexes found."); return; } - printTable( - ["ID", "NAME", "REGION", "TYPE", "SIMILARITY", "DIMENSIONS"], - indexes.map((i) => [ - i.id, - i.name, - i.region, - i.type, - i.similarity_function, - String(i.dimension_count), - ]), - ); + printJSON(indexes); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/rename.ts b/src/commands/vector/rename.ts index 31e150d..494fc34 100644 --- a/src/commands/vector/rename.ts +++ b/src/commands/vector/rename.ts @@ -1,34 +1,24 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { VectorIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; name: string } - export function registerVectorRename(vector: Command): void { vector - .command("rename ") + .command("rename") .description("Rename a vector index") + .requiredOption("--index-id ", "Vector index ID") .requiredOption("--name ", "New index name") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; name: string }) => { const auth = resolveAuth(flags); try { - const idx = await request( - auth, - "POST", - `/v2/vector/index/${indexId}/rename`, - { name: flags.name }, - ); - if (flags.json) { printJSON(idx); return; } - console.log(`Index renamed to '${idx.name}'.`); - console.log(); - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/rename`, { name: flags.name }); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/reset-password.ts b/src/commands/vector/reset-password.ts index b9def72..c64e0a2 100644 --- a/src/commands/vector/reset-password.ts +++ b/src/commands/vector/reset-password.ts @@ -1,32 +1,23 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, printKeyValue, handleError } from "../../output.js"; +import { printJSON, handleError } from "../../output.js"; import type { VectorIndex } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean } - export function registerVectorResetPassword(vector: Command): void { vector - .command("reset-password ") + .command("reset-password") .description("Reset tokens for a vector index") + .requiredOption("--index-id ", "Vector index ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const idx = await request( - auth, - "POST", - `/v2/vector/index/${indexId}/reset-password`, - ); - if (flags.json) { printJSON(idx); return; } - console.log("Tokens reset successfully."); - console.log(); - printKeyValue(idx as unknown as Record); + const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/reset-password`); + printJSON(idx); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/set-plan.ts b/src/commands/vector/set-plan.ts index 304dd48..7de6acc 100644 --- a/src/commands/vector/set-plan.ts +++ b/src/commands/vector/set-plan.ts @@ -4,26 +4,21 @@ import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; import { VECTOR_PLANS } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; plan: string } - export function registerVectorSetPlan(vector: Command): void { vector - .command("set-plan ") + .command("set-plan") .description(`Change the plan of a vector index. Plans: ${VECTOR_PLANS.join(", ")}`) + .requiredOption("--index-id ", "Vector index ID") .requiredOption("--plan ", `Target plan (${VECTOR_PLANS.join(", ")})`) .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; plan: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/vector/index/${indexId}/setplan`, { - target_plan: flags.plan, - }); - if (flags.json) { printJSON({ success: true, index_id: indexId, plan: flags.plan }); return; } - console.log(`Plan changed to '${flags.plan}'.`); + const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/setplan`, { target_plan: flags.plan }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/stats.ts b/src/commands/vector/stats.ts index 4f8577c..ba6be79 100644 --- a/src/commands/vector/stats.ts +++ b/src/commands/vector/stats.ts @@ -4,52 +4,37 @@ import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; import { STATS_PERIODS } from "../../types.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; period?: string } - export function registerVectorStats(vector: Command): void { vector .command("stats") .description("Get statistics across all vector indexes") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (flags: Flags) => { + .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); try { - const stats = await request>( - auth, - "GET", - "/v2/vector/index/stats", - ); + const stats = await request>(auth, "GET", "/v2/vector/index/stats"); printJSON(stats); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); vector - .command("index-stats ") + .command("index-stats") .description("Get statistics for a specific vector index") - .option( - "--period ", - `Time period for aggregation. Available: ${STATS_PERIODS.join(", ")}`, - "1h", - ) + .requiredOption("--index-id ", "Vector index ID") + .option("--period ", `Time period. Available: ${STATS_PERIODS.join(", ")}`, "1h") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); const qs = flags.period ? `?period=${flags.period}` : ""; try { - const stats = await request>( - auth, - "GET", - `/v2/vector/index/${indexId}/stats${qs}`, - ); + const stats = await request>(auth, "GET", `/v2/vector/index/${flags.indexId}/stats${qs}`); printJSON(stats); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/commands/vector/transfer.ts b/src/commands/vector/transfer.ts index d69b770..559930c 100644 --- a/src/commands/vector/transfer.ts +++ b/src/commands/vector/transfer.ts @@ -3,29 +3,21 @@ import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON, handleError } from "../../output.js"; -interface Flags { email?: string; apiKey?: string; json?: boolean; targetAccount: string } - export function registerVectorTransfer(vector: Command): void { vector - .command("transfer ") + .command("transfer") .description("Transfer a vector index to another team") - .requiredOption("--target-account ", "Target team ID") + .requiredOption("--index-id ", "Vector index ID") + .requiredOption("--target-account ", "Target team ID") .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") - .option("--json", "Output as JSON") - .action(async (indexId: string, flags: Flags) => { + .action(async (flags: { indexId: string; email?: string; apiKey?: string; targetAccount: string }) => { const auth = resolveAuth(flags); try { - await request(auth, "POST", `/v2/vector/index/${indexId}/transfer`, { - target_account: flags.targetAccount, - }); - if (flags.json) { - printJSON({ success: true, index_id: indexId, target_account: flags.targetAccount }); - return; - } - console.log(`Index ${indexId} transferred to team ${flags.targetAccount}.`); + const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); + printJSON(result); } catch (err) { - handleError(err, flags.json ?? false); + handleError(err); } }); } diff --git a/src/output.ts b/src/output.ts index dc501d7..17ec41b 100644 --- a/src/output.ts +++ b/src/output.ts @@ -2,36 +2,8 @@ export function printJSON(data: unknown): void { console.log(JSON.stringify(data, null, 2)); } -export function printTable(headers: string[], rows: string[][]): void { - const colWidths = headers.map((h, i) => - Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)), - ); - const formatRow = (row: string[]) => - row.map((val, i) => val.padEnd(colWidths[i]!)).join(" "); - console.log(formatRow(headers)); - for (const row of rows) { - console.log(formatRow(row)); - } -} - -export function printKeyValue(obj: Record): void { - const maxKeyLen = Math.max(...Object.keys(obj).map((k) => k.length)); - for (const [key, val] of Object.entries(obj)) { - const formatted = Array.isArray(val) - ? val.join(", ") - : val === null || val === undefined - ? "" - : String(val); - console.log(`${key.padEnd(maxKeyLen + 2)}${formatted}`); - } -} - -export function handleError(err: unknown, json: boolean): never { +export function handleError(err: unknown): never { const message = err instanceof Error ? err.message : String(err); - if (json) { - console.error(JSON.stringify({ error: message })); - } else { - console.error(`Error: ${message}`); - } + console.error(JSON.stringify({ error: message })); process.exit(1); } diff --git a/src/types.ts b/src/types.ts index 40dc453..fec1d95 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,8 @@ export interface Database { consistent?: boolean; daily_backup_enabled?: boolean; region?: string; + rest_token?: string; + read_only_rest_token?: string; db_max_clients?: number; db_memory_threshold?: number; db_disk_threshold?: number; From cb8f5807c554f1962a264d7cc0b8898eb98746d4 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Tue, 31 Mar 2026 17:03:10 +0300 Subject: [PATCH 03/18] fix: copilet reviews --- package.json | 3 ++- src/auth.ts | 2 +- src/cli.ts | 9 +++++++-- src/commands/qstash/stats.ts | 2 +- src/commands/qstash/update-budget.ts | 3 +++ src/commands/redis/exec.ts | 3 ++- src/commands/redis/update-budget.ts | 3 +++ src/commands/search/stats.ts | 2 +- src/commands/team/add-member.ts | 3 +-- src/commands/vector/create.ts | 3 +++ src/commands/vector/stats.ts | 2 +- tsconfig.json | 14 +++++++++++--- 12 files changed, 36 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 8f71d40..3eedac4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ }, "scripts": { "build": "tsc", - "dev": "tsc --watch" + "dev": "tsc --watch", + "prepublishOnly": "npm run build" }, "keywords": ["upstash", "cli"], "author": "Upstash", diff --git a/src/auth.ts b/src/auth.ts index 84a58d2..9bf2993 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -9,7 +9,7 @@ export function resolveAuth(flags: { email?: string; apiKey?: string }): Auth { if (!email || !apiKey) { console.error( - JSON.stringify({ error: "Authentication required. Set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables." }), + JSON.stringify({ error: "Authentication required. Provide credentials via --email and --api-key flags or set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables." }), ); process.exit(1); } diff --git a/src/cli.ts b/src/cli.ts index 82acabf..cd94fd7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,7 @@ #!/usr/bin/env node import { Command } from "commander"; +import pkg from "../package.json" with { type: "json" }; +const { version } = pkg; import { registerRedis } from "./commands/redis/index.js"; import { registerTeam } from "./commands/team/index.js"; import { registerVector } from "./commands/vector/index.js"; @@ -11,7 +13,7 @@ const program = new Command(); program .name("upstash") .description("Agent-friendly CLI for Upstash") - .version("1.0.0"); + .version(version); registerRedis(program); registerTeam(program); @@ -19,4 +21,7 @@ registerVector(program); registerSearch(program); registerQStash(program); -program.parse(); +program.parseAsync().catch((err) => { + console.error(err); + process.exitCode = 1; +}); diff --git a/src/commands/qstash/stats.ts b/src/commands/qstash/stats.ts index 19e7b1b..965cade 100644 --- a/src/commands/qstash/stats.ts +++ b/src/commands/qstash/stats.ts @@ -14,7 +14,7 @@ export function registerQStashStats(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); - const qs = flags.period ? `?period=${flags.period}` : ""; + const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; try { const stats = await request>(auth, "GET", `/v2/qstash/stats/${flags.qstashId}${qs}`); printJSON(stats); diff --git a/src/commands/qstash/update-budget.ts b/src/commands/qstash/update-budget.ts index 7e221f4..a90f15e 100644 --- a/src/commands/qstash/update-budget.ts +++ b/src/commands/qstash/update-budget.ts @@ -12,6 +12,9 @@ export function registerQStashUpdateBudget(qstash: Command): void { .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string; budget: number }) => { + if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { + handleError(new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (dollars).`)); + } const auth = resolveAuth(flags); try { const result = await request(auth, "PATCH", `/v2/qstash/update-budget/${flags.qstashId}`, { budget: flags.budget }); diff --git a/src/commands/redis/exec.ts b/src/commands/redis/exec.ts index 99889b9..8fc597a 100644 --- a/src/commands/redis/exec.ts +++ b/src/commands/redis/exec.ts @@ -52,7 +52,8 @@ function parseCommand(input: string): string[] { let inDouble = false; for (let i = 0; i < input.length; i++) { - const ch = input[i]!; + const ch = input[i]; + if (ch === undefined) break; if (ch === "'" && !inDouble) { inSingle = !inSingle; diff --git a/src/commands/redis/update-budget.ts b/src/commands/redis/update-budget.ts index 87ffe94..2315c87 100644 --- a/src/commands/redis/update-budget.ts +++ b/src/commands/redis/update-budget.ts @@ -12,6 +12,9 @@ export function registerUpdateBudget(redis: Command): void { .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; budget: number; email?: string; apiKey?: string }) => { + if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { + handleError(new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (cents).`)); + } const auth = resolveAuth(flags); try { const result = await request(auth, "PATCH", `/v2/redis/update-budget/${flags.dbId}`, { budget: flags.budget }); diff --git a/src/commands/search/stats.ts b/src/commands/search/stats.ts index bc7de0b..f6c1fbf 100644 --- a/src/commands/search/stats.ts +++ b/src/commands/search/stats.ts @@ -29,7 +29,7 @@ export function registerSearchStats(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); - const qs = flags.period ? `?period=${flags.period}` : ""; + const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; try { const stats = await request>(auth, "GET", `/v2/search/${flags.indexId}/stats${qs}`); printJSON(stats); diff --git a/src/commands/team/add-member.ts b/src/commands/team/add-member.ts index 8828487..947b07f 100644 --- a/src/commands/team/add-member.ts +++ b/src/commands/team/add-member.ts @@ -16,8 +16,7 @@ export function registerTeamAddMember(team: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; teamId: string; memberEmail: string; role: string }) => { if (!(TEAM_MEMBER_ROLES as readonly string[]).includes(flags.role)) { - console.error(JSON.stringify({ error: `Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}` })); - process.exit(1); + handleError(new Error(`Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`)); } const auth = resolveAuth(flags); try { diff --git a/src/commands/vector/create.ts b/src/commands/vector/create.ts index 78483a3..32295b6 100644 --- a/src/commands/vector/create.ts +++ b/src/commands/vector/create.ts @@ -20,6 +20,9 @@ export function registerVectorCreate(vector: Command): void { .option("--email ", "Upstash email") .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; similarityFunction: string; dimensionCount: number; type?: string; embeddingModel?: string; indexType?: string; sparseEmbeddingModel?: string }) => { + if (!Number.isFinite(flags.dimensionCount) || !Number.isInteger(flags.dimensionCount) || flags.dimensionCount < 0) { + handleError(new Error(`Invalid --dimension-count: "${flags.dimensionCount}". Must be a non-negative integer.`)); + } const auth = resolveAuth(flags); try { const idx = await request(auth, "POST", "/v2/vector/index", { diff --git a/src/commands/vector/stats.ts b/src/commands/vector/stats.ts index ba6be79..0d4cde4 100644 --- a/src/commands/vector/stats.ts +++ b/src/commands/vector/stats.ts @@ -29,7 +29,7 @@ export function registerVectorStats(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); - const qs = flags.period ? `?period=${flags.period}` : ""; + const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; try { const stats = await request>(auth, "GET", `/v2/vector/index/${flags.indexId}/stats${qs}`); printJSON(stats); diff --git a/tsconfig.json b/tsconfig.json index df0f4fd..625b1a0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,22 @@ { "compilerOptions": { "target": "ES2022", - "module": "Node16", - "moduleResolution": "Node16", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true }, "include": ["src"] } From a340396bdff423dcc713f8b7bad1af1a59025900 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Tue, 31 Mar 2026 18:01:59 +0300 Subject: [PATCH 04/18] feat: add tests --- .env.example | 2 + .github/workflows/release.yml | 61 +- package-lock.json | 1409 +++++++++++++++++++++++++++++- package.json | 8 +- src/commands/redis/exec.ts | 2 +- tests/helpers/program.ts | 61 ++ tests/integration/qstash.test.ts | 51 ++ tests/integration/redis.test.ts | 210 +++++ tests/integration/search.test.ts | 76 ++ tests/integration/team.test.ts | 49 ++ tests/integration/vector.test.ts | 79 ++ tests/unit/parseCommand.test.ts | 40 + tsconfig.test.json | 8 + vitest.config.ts | 14 + 14 files changed, 2016 insertions(+), 54 deletions(-) create mode 100644 .env.example create mode 100644 tests/helpers/program.ts create mode 100644 tests/integration/qstash.test.ts create mode 100644 tests/integration/redis.test.ts create mode 100644 tests/integration/search.test.ts create mode 100644 tests/integration/team.test.ts create mode 100644 tests/integration/vector.test.ts create mode 100644 tests/unit/parseCommand.test.ts create mode 100644 tsconfig.test.json create mode 100644 vitest.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ca6f75e --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +UPSTASH_EMAIL=you@example.com +UPSTASH_API_KEY=your_dev_api_key diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45c9043..ae505f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,62 +10,23 @@ jobs: name: Release on npm runs-on: ubuntu-latest steps: - - name: Checkout Repo - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Set env - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - - name: Setup Node - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 16 - - - run: curl -fsSL https://deno.land/x/install/install.sh | sh - - - run: echo "$HOME/.deno/bin" > $GITHUB_PATH + node-version: 20 + registry-url: https://registry.npmjs.org - - name: Build - run: deno run -A ./cmd/build.ts $VERSION + - run: npm ci - name: Publish if: "!github.event.release.prerelease" - working-directory: ./dist - run: | - echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" > .npmrc - npm publish --access public + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish release candidate - if: "github.event.release.prerelease" - working-directory: ./dist - run: | - echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" > .npmrc - npm publish --access public --tag=next - - binaries: - name: Release binary - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - arch: - [ - "x86_64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", - "x86_64-apple-darwin", - "aarch64-apple-darwin", - ] - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - - - run: curl -fsSL https://deno.land/x/install/install.sh | sh - - run: echo "$HOME/.deno/bin" > $GITHUB_PATH - - - name: compile - run: deno compile --allow-env --allow-read --allow-write --allow-net --target=${{ matrix.arch }} --output=./bin/upstash_${{ matrix.arch}} ./src/mod.ts - - - name: upload - run: gh release upload ${GITHUB_REF#refs/*/} ./bin/* + if: github.event.release.prerelease + run: npm publish --access public --tag next env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package-lock.json b/package-lock.json index 697c2f0..4c272ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,768 @@ }, "devDependencies": { "@types/node": "^20.10.0", - "typescript": "^5.3.0" + "typescript": "^5.3.0", + "vitest": "^2.0.0" }, "engines": { "node": ">=18.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", @@ -32,6 +788,166 @@ "undici-types": "~6.21.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -41,6 +957,331 @@ "node": ">=18" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -61,6 +1302,172 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 3eedac4..77f2ed8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc -p tsconfig.test.json --noEmit" }, "keywords": ["upstash", "cli"], "author": "Upstash", @@ -23,7 +26,8 @@ }, "devDependencies": { "@types/node": "^20.10.0", - "typescript": "^5.3.0" + "typescript": "^5.3.0", + "vitest": "^2.0.0" }, "engines": { "node": ">=18.0.0" diff --git a/src/commands/redis/exec.ts b/src/commands/redis/exec.ts index 8fc597a..2ae0d41 100644 --- a/src/commands/redis/exec.ts +++ b/src/commands/redis/exec.ts @@ -45,7 +45,7 @@ export function registerExec(redis: Command): void { }); } -function parseCommand(input: string): string[] { +export function parseCommand(input: string): string[] { const args: string[] = []; let current = ""; let inSingle = false; diff --git a/tests/helpers/program.ts b/tests/helpers/program.ts new file mode 100644 index 0000000..a13a010 --- /dev/null +++ b/tests/helpers/program.ts @@ -0,0 +1,61 @@ +import { Command } from "commander"; + +export async function createRedisProgram(): Promise { + const { registerRedis } = await import("../../src/commands/redis/index.js"); + const p = new Command().exitOverride(); + registerRedis(p); + return p; +} + +export async function createVectorProgram(): Promise { + const { registerVector } = await import("../../src/commands/vector/index.js"); + const p = new Command().exitOverride(); + registerVector(p); + return p; +} + +export async function createSearchProgram(): Promise { + const { registerSearch } = await import("../../src/commands/search/index.js"); + const p = new Command().exitOverride(); + registerSearch(p); + return p; +} + +export async function createQStashProgram(): Promise { + const { registerQStash } = await import("../../src/commands/qstash/index.js"); + const p = new Command().exitOverride(); + registerQStash(p); + return p; +} + +export async function createTeamProgram(): Promise { + const { registerTeam } = await import("../../src/commands/team/index.js"); + const p = new Command().exitOverride(); + registerTeam(p); + return p; +} + +export async function runCommand(program: Command, argv: string[]): Promise { + const output: string[] = []; + const errors: string[] = []; + const origLog = console.log; + const origError = console.error; + const origExit = process.exit; + + console.log = (...args: unknown[]) => output.push(args.join(" ")); + console.error = (...args: unknown[]) => errors.push(args.join(" ")); + process.exit = ((code?: number) => { + throw new Error(`CLI error (exit ${code ?? 1}): ${errors.at(-1) ?? "unknown error"}`); + }) as typeof process.exit; + + try { + await program.parseAsync(["node", "upstash", ...argv]); + } finally { + console.log = origLog; + console.error = origError; + process.exit = origExit; + } + + const last = output.at(-1); + return last ? JSON.parse(last) as unknown : null; +} diff --git a/tests/integration/qstash.test.ts b/tests/integration/qstash.test.ts new file mode 100644 index 0000000..f4fff1d --- /dev/null +++ b/tests/integration/qstash.test.ts @@ -0,0 +1,51 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import { createQStashProgram, runCommand } from "../helpers/program.js"; +import type { QStashUser } from "../../src/types.js"; + +let qstashId: string | undefined; + +beforeAll(async () => { + const p = await createQStashProgram(); + const instances = await runCommand(p, ["qstash", "list"]) as QStashUser[]; + expect(Array.isArray(instances)).toBe(true); + expect(instances.length).toBeGreaterThan(0); + qstashId = instances[0]?.id; + expect(qstashId).toBeDefined(); +}); + +describe("qstash list", () => { + it("returns at least one instance", async () => { + const p = await createQStashProgram(); + const instances = await runCommand(p, ["qstash", "list"]) as QStashUser[]; + expect(Array.isArray(instances)).toBe(true); + expect(instances.length).toBeGreaterThan(0); + }); +}); + +describe("qstash get", () => { + it("returns the instance with expected fields", async () => { + const p = await createQStashProgram(); + const instance = await runCommand(p, ["qstash", "get", "--qstash-id", qstashId!]) as QStashUser; + expect(instance.id).toBe(qstashId); + expect(instance.token).toBeDefined(); + expect(instance.region).toBeDefined(); + expect(instance.state).toBeDefined(); + }); +}); + +describe("qstash stats", () => { + it("returns stats for the instance", async () => { + const p = await createQStashProgram(); + const stats = await runCommand(p, ["qstash", "stats", "--qstash-id", qstashId!]) as Record; + expect(stats).toBeDefined(); + expect(typeof stats).toBe("object"); + }); +}); + +describe("qstash ipv4", () => { + it("returns an array of CIDR blocks", async () => { + const p = await createQStashProgram(); + const result = await runCommand(p, ["qstash", "ipv4"]) as unknown; + expect(result).toBeDefined(); + }); +}); diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts new file mode 100644 index 0000000..1e8e4fc --- /dev/null +++ b/tests/integration/redis.test.ts @@ -0,0 +1,210 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createRedisProgram, runCommand } from "../helpers/program.js"; +import type { Database, Backup } from "../../src/types.js"; + +const TEST_NAME = `cli-test-${Date.now()}`; +let dbId: string | undefined; +let dbEndpoint: string | undefined; +let dbRestToken: string | undefined; + +beforeAll(async () => { + const p = await createRedisProgram(); + const db = await runCommand(p, [ + "redis", "create", + "--name", TEST_NAME, + "--region", "us-east-1", + ]) as Database; + + expect(db.database_id).toBeDefined(); + dbId = db.database_id; + dbEndpoint = db.endpoint; + dbRestToken = db.rest_token; +}); + +afterAll(async () => { + if (!dbId) return; + const p = await createRedisProgram(); + await runCommand(p, ["redis", "delete", "--db-id", dbId]); +}); + +describe("redis list", () => { + it("includes the created database", async () => { + const p = await createRedisProgram(); + const dbs = await runCommand(p, ["redis", "list"]) as Database[]; + expect(Array.isArray(dbs)).toBe(true); + expect(dbs.some((db) => db.database_id === dbId)).toBe(true); + }); +}); + +describe("redis get", () => { + it("returns the database by id", async () => { + const p = await createRedisProgram(); + const db = await runCommand(p, ["redis", "get", "--db-id", dbId!]) as Database; + expect(db.database_id).toBe(dbId); + expect(db.database_name).toBe(TEST_NAME); + }); + + it("omits password with --hide-credentials", async () => { + const p = await createRedisProgram(); + const db = await runCommand(p, ["redis", "get", "--db-id", dbId!, "--hide-credentials"]) as Database; + expect(db.password).toBeUndefined(); + }); +}); + +describe("redis rename", () => { + it("updates the database name", async () => { + const newName = `${TEST_NAME}-renamed`; + const p = await createRedisProgram(); + await runCommand(p, ["redis", "rename", "--db-id", dbId!, "--name", newName]); + + const p2 = await createRedisProgram(); + const db = await runCommand(p2, ["redis", "get", "--db-id", dbId!]) as Database; + + expect(db.database_name).toBe(newName); + + // rename back so subsequent tests aren't affected + const p3 = await createRedisProgram(); + await runCommand(p3, ["redis", "rename", "--db-id", dbId!, "--name", TEST_NAME]); + }); +}); + +describe("redis stats", () => { + it("returns a stats object", async () => { + const p = await createRedisProgram(); + const stats = await runCommand(p, ["redis", "stats", "--db-id", dbId!]) as Record; + expect(stats).toBeDefined(); + expect(typeof stats).toBe("object"); + }); +}); + +describe("redis eviction", () => { + it("enables and disables eviction", async () => { + const p1 = await createRedisProgram(); + const enabled = await runCommand(p1, ["redis", "enable-eviction", "--db-id", dbId!]) as Record; + expect(enabled).toBeDefined(); + + const p2 = await createRedisProgram(); + const disabled = await runCommand(p2, ["redis", "disable-eviction", "--db-id", dbId!]) as Record; + expect(disabled).toBeDefined(); + }); +}); + +describe("redis autoupgrade", () => { + it("enables and disables autoupgrade", async () => { + const p1 = await createRedisProgram(); + await runCommand(p1, ["redis", "enable-autoupgrade", "--db-id", dbId!]); + + const p2 = await createRedisProgram(); + await runCommand(p2, ["redis", "disable-autoupgrade", "--db-id", dbId!]); + + const p3 = await createRedisProgram(); + const db = await runCommand(p3, ["redis", "get", "--db-id", dbId!]) as Database; + expect(db.auto_upgrade).toBe(false); + }); +}); + +describe("redis backup", () => { + let backupId: string | undefined; + + it("creates a backup and appears in list", async () => { + // wait for DB to be fully active before triggering a backup + await new Promise((r) => setTimeout(r, 5000)); + + const p = await createRedisProgram(); + await runCommand(p, [ + "redis", "backup", "create", + "--db-id", dbId!, + "--name", "test-backup", + ]); + + await new Promise((r) => setTimeout(r, 10000)); + const p2 = await createRedisProgram(); + const backups = await runCommand(p2, ["redis", "backup", "list", "--db-id", dbId!]) as Backup[]; + + expect(Array.isArray(backups)).toBe(true); + expect(backups.length).toBeGreaterThan(0); + backupId = backups[0]?.backup_id; + expect(backupId).toBeDefined(); + }); + + it("deletes the backup (dry-run)", async () => { + if (!backupId) return; + const p = await createRedisProgram(); + const result = await runCommand(p, [ + "redis", "backup", "delete", + "--db-id", dbId!, + "--backup-id", backupId, + "--dry-run", + ]) as Record; + expect(result["dry_run"]).toBe(true); + }); + + it("deletes the backup", async () => { + if (!backupId) return; + const p = await createRedisProgram(); + await runCommand(p, [ + "redis", "backup", "delete", + "--db-id", dbId!, + "--backup-id", backupId, + ]); + }); +}); + +describe("redis delete dry-run", () => { + it("returns dry_run: true without deleting", async () => { + const p = await createRedisProgram(); + const result = await runCommand(p, ["redis", "delete", "--db-id", dbId!, "--dry-run"]) as Record; + expect(result["dry_run"]).toBe(true); + expect(result["database_id"]).toBe(dbId); + }); +}); + +describe("redis exec", () => { + it("executes SET and GET commands against the database", async () => { + if (!dbEndpoint || !dbRestToken) { + console.warn("Skipping exec tests: endpoint or rest_token not available"); + return; + } + + const dbUrl = `https://${dbEndpoint}`; + + const p1 = await createRedisProgram(); + const setResult = await runCommand(p1, [ + "redis", "exec", + "--db-url", dbUrl, + "--db-token", dbRestToken, + "--command", "SET cli-test-key hello", + ]) as { result: unknown }; + expect(setResult.result).toBe("OK"); + + const p2 = await createRedisProgram(); + const getResult = await runCommand(p2, [ + "redis", "exec", + "--db-url", dbUrl, + "--db-token", dbRestToken, + "--command", "GET cli-test-key", + ]) as { result: unknown }; + expect(getResult.result).toBe("hello"); + + const p3 = await createRedisProgram(); + await runCommand(p3, [ + "redis", "exec", + "--db-url", dbUrl, + "--db-token", dbRestToken, + "--command", "DEL cli-test-key", + ]); + }); + + it("executes PING", async () => { + if (!dbEndpoint || !dbRestToken) return; + + const p = await createRedisProgram(); + const result = await runCommand(p, [ + "redis", "exec", + "--db-url", `https://${dbEndpoint}`, + "--db-token", dbRestToken, + "--command", "PING", + ]) as { result: unknown }; + expect(result.result).toBe("PONG"); + }); +}); diff --git a/tests/integration/search.test.ts b/tests/integration/search.test.ts new file mode 100644 index 0000000..ccecb71 --- /dev/null +++ b/tests/integration/search.test.ts @@ -0,0 +1,76 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createSearchProgram, runCommand } from "../helpers/program.js"; +import type { SearchIndex } from "../../src/types.js"; + +const TEST_NAME = `cli-test-${Date.now()}`; +let indexId: string | undefined; + +beforeAll(async () => { + const p = await createSearchProgram(); + const idx = await runCommand(p, [ + "search", "create", + "--name", TEST_NAME, + "--region", "eu-west-1", + "--type", "payg", + ]) as SearchIndex; + + expect(idx.id).toBeDefined(); + indexId = idx.id; +}); + +afterAll(async () => { + if (!indexId) return; + const p = await createSearchProgram(); + await runCommand(p, ["search", "delete", "--index-id", indexId]); +}); + +describe("search list", () => { + it("includes the created index", async () => { + const p = await createSearchProgram(); + const indexes = await runCommand(p, ["search", "list"]) as SearchIndex[]; + expect(Array.isArray(indexes)).toBe(true); + expect(indexes.some((idx) => idx.id === indexId)).toBe(true); + }); +}); + +describe("search get", () => { + it("returns the index by id", async () => { + const p = await createSearchProgram(); + const idx = await runCommand(p, ["search", "get", "--index-id", indexId!]) as SearchIndex; + expect(idx.id).toBe(indexId); + expect(idx.name).toBe(TEST_NAME); + }); +}); + +describe("search rename", () => { + it("updates the index name", async () => { + const newName = `${TEST_NAME}-renamed`; + const p = await createSearchProgram(); + await runCommand(p, ["search", "rename", "--index-id", indexId!, "--name", newName]); + + const p2 = await createSearchProgram(); + const idx = await runCommand(p2, ["search", "get", "--index-id", indexId!]) as SearchIndex; + expect(idx.name).toBe(newName); + + const p3 = await createSearchProgram(); + await runCommand(p3, ["search", "rename", "--index-id", indexId!, "--name", TEST_NAME]); + }); +}); + +describe("search index-stats", () => { + it("returns stats for the index", async () => { + const p = await createSearchProgram(); + const stats = await runCommand(p, ["search", "index-stats", "--index-id", indexId!]) as Record; + expect(stats).toBeDefined(); + expect(typeof stats).toBe("object"); + }); +}); + +describe("search delete dry-run", () => { + it("returns dry_run: true without deleting", async () => { + const p = await createSearchProgram(); + const result = await runCommand(p, ["search", "delete", "--index-id", indexId!, "--dry-run"]) as Record; + expect(result["dry_run"]).toBe(true); + expect(result["index_id"]).toBe(indexId); + }); +}); diff --git a/tests/integration/team.test.ts b/tests/integration/team.test.ts new file mode 100644 index 0000000..0c627f9 --- /dev/null +++ b/tests/integration/team.test.ts @@ -0,0 +1,49 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createTeamProgram, runCommand } from "../helpers/program.js"; +import type { Team, TeamMember } from "../../src/types.js"; + +const TEST_NAME = `cli-test-${Date.now()}`; +let teamId: string | undefined; + +beforeAll(async () => { + const p = await createTeamProgram(); + const team = await runCommand(p, ["team", "create", "--name", TEST_NAME]) as Team; + expect(team.team_id).toBeDefined(); + teamId = team.team_id; +}); + +afterAll(async () => { + if (!teamId) return; + const p = await createTeamProgram(); + await runCommand(p, ["team", "delete", "--team-id", teamId]); +}); + +describe("team list", () => { + it("includes the created team", async () => { + const p = await createTeamProgram(); + const teams = await runCommand(p, ["team", "list"]) as Team[]; + expect(Array.isArray(teams)).toBe(true); + expect(teams.some((t) => t.team_id === teamId)).toBe(true); + }); +}); + +describe("team members", () => { + it("returns the owner as a member", async () => { + const p = await createTeamProgram(); + const members = await runCommand(p, ["team", "members", "--team-id", teamId!]) as TeamMember[]; + expect(Array.isArray(members)).toBe(true); + expect(members.length).toBeGreaterThan(0); + const owner = members.find((m) => m.member_role === "owner"); + expect(owner).toBeDefined(); + expect(owner?.member_email).toBeDefined(); + }); +}); + +describe("team delete dry-run", () => { + it("returns dry_run: true without deleting", async () => { + const p = await createTeamProgram(); + const result = await runCommand(p, ["team", "delete", "--team-id", teamId!, "--dry-run"]) as Record; + expect(result["dry_run"]).toBe(true); + expect(result["team_id"]).toBe(teamId); + }); +}); diff --git a/tests/integration/vector.test.ts b/tests/integration/vector.test.ts new file mode 100644 index 0000000..8e4e981 --- /dev/null +++ b/tests/integration/vector.test.ts @@ -0,0 +1,79 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createVectorProgram, runCommand } from "../helpers/program.js"; +import type { VectorIndex } from "../../src/types.js"; + +const TEST_NAME = `cli-test-${Date.now()}`; +let indexId: string | undefined; + +beforeAll(async () => { + const p = await createVectorProgram(); + const idx = await runCommand(p, [ + "vector", "create", + "--name", TEST_NAME, + "--region", "us-east-1", + "--similarity-function", "COSINE", + "--dimension-count", "1536", + ]) as VectorIndex; + + expect(idx.id).toBeDefined(); + indexId = idx.id; +}); + +afterAll(async () => { + if (!indexId) return; + const p = await createVectorProgram(); + await runCommand(p, ["vector", "delete", "--index-id", indexId]); +}); + +describe("vector list", () => { + it("includes the created index", async () => { + const p = await createVectorProgram(); + const indexes = await runCommand(p, ["vector", "list"]) as VectorIndex[]; + expect(Array.isArray(indexes)).toBe(true); + expect(indexes.some((idx) => idx.id === indexId)).toBe(true); + }); +}); + +describe("vector get", () => { + it("returns the index by id", async () => { + const p = await createVectorProgram(); + const idx = await runCommand(p, ["vector", "get", "--index-id", indexId!]) as VectorIndex; + expect(idx.id).toBe(indexId); + expect(idx.name).toBe(TEST_NAME); + expect(idx.similarity_function).toBe("COSINE"); + expect(idx.dimension_count).toBe(1536); + }); +}); + +describe("vector rename", () => { + it("updates the index name", async () => { + const newName = `${TEST_NAME}-renamed`; + const p = await createVectorProgram(); + await runCommand(p, ["vector", "rename", "--index-id", indexId!, "--name", newName]); + + const p2 = await createVectorProgram(); + const idx = await runCommand(p2, ["vector", "get", "--index-id", indexId!]) as VectorIndex; + expect(idx.name).toBe(newName); + + const p3 = await createVectorProgram(); + await runCommand(p3, ["vector", "rename", "--index-id", indexId!, "--name", TEST_NAME]); + }); +}); + +describe("vector index-stats", () => { + it("returns stats for the index", async () => { + const p = await createVectorProgram(); + const stats = await runCommand(p, ["vector", "index-stats", "--index-id", indexId!]) as Record; + expect(stats).toBeDefined(); + expect(typeof stats).toBe("object"); + }); +}); + +describe("vector delete dry-run", () => { + it("returns dry_run: true without deleting", async () => { + const p = await createVectorProgram(); + const result = await runCommand(p, ["vector", "delete", "--index-id", indexId!, "--dry-run"]) as Record; + expect(result["dry_run"]).toBe(true); + expect(result["index_id"]).toBe(indexId); + }); +}); diff --git a/tests/unit/parseCommand.test.ts b/tests/unit/parseCommand.test.ts new file mode 100644 index 0000000..7c1ddbe --- /dev/null +++ b/tests/unit/parseCommand.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { parseCommand } from "../../src/commands/redis/exec.js"; + +describe("parseCommand", () => { + it("splits simple tokens", () => { + expect(parseCommand("SET key value")).toEqual(["SET", "key", "value"]); + }); + + it("handles double-quoted string with spaces", () => { + expect(parseCommand('SET key "hello world"')).toEqual(["SET", "key", "hello world"]); + }); + + it("handles single-quoted string with spaces", () => { + expect(parseCommand("SET key 'hello world'")).toEqual(["SET", "key", "hello world"]); + }); + + it("strips surrounding quotes", () => { + expect(parseCommand('"SET" key')).toEqual(["SET", "key"]); + }); + + it("returns empty array for empty string", () => { + expect(parseCommand("")).toEqual([]); + }); + + it("ignores extra spaces between tokens", () => { + expect(parseCommand("SET key value")).toEqual(["SET", "key", "value"]); + }); + + it("handles single quotes inside double-quoted string", () => { + expect(parseCommand(`SET key "it's fine"`)).toEqual(["SET", "key", "it's fine"]); + }); + + it("handles double quotes inside single-quoted string", () => { + expect(parseCommand(`SET key 'say "hello"'`)).toEqual(["SET", "key", 'say "hello"']); + }); + + it("handles single token with no spaces", () => { + expect(parseCommand("PING")).toEqual(["PING"]); + }); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..508201b --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src", "tests"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..3decdb5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.ts"], + pool: "forks", + testTimeout: 30000, + hookTimeout: 30000, + envDir: ".", + fileParallelism: false, + }, + resolve: { conditions: ["node", "import"] }, +}); From 4c2c5c04f55fb6ef9d961100700073ddfc6ff087 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Thu, 2 Apr 2026 13:47:23 +0300 Subject: [PATCH 05/18] fix: update release workflow to use Node.js 24 and improve npm publish commands --- .github/workflows/release.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae505f7..07987c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,10 @@ on: types: - published +permissions: + id-token: write + contents: read + jobs: npm: name: Release on npm @@ -14,19 +18,15 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 registry-url: https://registry.npmjs.org - run: npm ci - - name: Publish - if: "!github.event.release.prerelease" - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Publish release candidate if: github.event.release.prerelease - run: npm publish --access public --tag next - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public --tag next --provenance + + - name: Publish + if: "!github.event.release.prerelease" + run: npm publish --access public --provenance From 4da3ee74f6ba0786b42fb00083167a5afc2e60d3 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Tue, 14 Apr 2026 17:17:17 +0300 Subject: [PATCH 06/18] fix: unify error handling --- src/cli.ts | 6 ++-- src/commands/qstash/disable-prodpack.ts | 10 ++---- src/commands/qstash/enable-prodpack.ts | 10 ++---- src/commands/qstash/get.ts | 10 ++---- src/commands/qstash/ipv4.ts | 10 ++---- src/commands/qstash/list.ts | 10 ++---- src/commands/qstash/move-to-team.ts | 10 ++---- src/commands/qstash/rotate-token.ts | 10 ++---- src/commands/qstash/set-plan.ts | 10 ++---- src/commands/qstash/stats.ts | 10 ++---- src/commands/qstash/update-budget.ts | 12 +++---- src/commands/redis/backup/create.ts | 10 ++---- src/commands/redis/backup/delete.ts | 10 ++---- src/commands/redis/backup/disable-daily.ts | 10 ++---- src/commands/redis/backup/enable-daily.ts | 10 ++---- src/commands/redis/backup/list.ts | 10 ++---- src/commands/redis/backup/restore.ts | 10 ++---- src/commands/redis/change-plan.ts | 10 ++---- src/commands/redis/create.ts | 23 ++++++-------- src/commands/redis/delete.ts | 10 ++---- src/commands/redis/disable-autoupgrade.ts | 10 ++---- src/commands/redis/disable-eviction.ts | 10 ++---- src/commands/redis/enable-autoupgrade.ts | 10 ++---- src/commands/redis/enable-eviction.ts | 10 ++---- src/commands/redis/enable-tls.ts | 10 ++---- src/commands/redis/exec.ts | 37 ++++++++++------------ src/commands/redis/get.ts | 10 ++---- src/commands/redis/list.ts | 10 ++---- src/commands/redis/move-to-team.ts | 10 ++---- src/commands/redis/rename.ts | 10 ++---- src/commands/redis/reset-password.ts | 10 ++---- src/commands/redis/stats.ts | 10 ++---- src/commands/redis/update-budget.ts | 12 +++---- src/commands/redis/update-regions.ts | 10 ++---- src/commands/search/create.ts | 10 ++---- src/commands/search/delete.ts | 10 ++---- src/commands/search/get.ts | 10 ++---- src/commands/search/list.ts | 10 ++---- src/commands/search/rename.ts | 10 ++---- src/commands/search/reset-password.ts | 10 ++---- src/commands/search/stats.ts | 18 +++-------- src/commands/search/transfer.ts | 10 ++---- src/commands/team/add-member.ts | 20 +++++------- src/commands/team/create.ts | 10 ++---- src/commands/team/delete.ts | 10 ++---- src/commands/team/list.ts | 10 ++---- src/commands/team/members.ts | 10 ++---- src/commands/team/remove-member.ts | 10 ++---- src/commands/vector/create.ts | 30 ++++++++---------- src/commands/vector/delete.ts | 10 ++---- src/commands/vector/get.ts | 10 ++---- src/commands/vector/list.ts | 10 ++---- src/commands/vector/rename.ts | 10 ++---- src/commands/vector/reset-password.ts | 10 ++---- src/commands/vector/set-plan.ts | 10 ++---- src/commands/vector/stats.ts | 18 +++-------- src/commands/vector/transfer.ts | 10 ++---- 57 files changed, 210 insertions(+), 446 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index cd94fd7..231e776 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,6 +7,7 @@ import { registerTeam } from "./commands/team/index.js"; import { registerVector } from "./commands/vector/index.js"; import { registerSearch } from "./commands/search/index.js"; import { registerQStash } from "./commands/qstash/index.js"; +import { handleError } from "./output.js"; const program = new Command(); @@ -21,7 +22,4 @@ registerVector(program); registerSearch(program); registerQStash(program); -program.parseAsync().catch((err) => { - console.error(err); - process.exitCode = 1; -}); +program.parseAsync().catch(handleError); diff --git a/src/commands/qstash/disable-prodpack.ts b/src/commands/qstash/disable-prodpack.ts index 54c7766..e861b3a 100644 --- a/src/commands/qstash/disable-prodpack.ts +++ b/src/commands/qstash/disable-prodpack.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerQStashDisableProdpack(qstash: Command): void { qstash @@ -12,11 +12,7 @@ export function registerQStashDisableProdpack(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/qstash/disable-prodpack/${flags.qstashId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/qstash/disable-prodpack/${flags.qstashId}`); + printJSON(result); }); } diff --git a/src/commands/qstash/enable-prodpack.ts b/src/commands/qstash/enable-prodpack.ts index 0b0e3b7..19dc0f0 100644 --- a/src/commands/qstash/enable-prodpack.ts +++ b/src/commands/qstash/enable-prodpack.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerQStashEnableProdpack(qstash: Command): void { qstash @@ -12,11 +12,7 @@ export function registerQStashEnableProdpack(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/qstash/enable-prodpack/${flags.qstashId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/qstash/enable-prodpack/${flags.qstashId}`); + printJSON(result); }); } diff --git a/src/commands/qstash/get.ts b/src/commands/qstash/get.ts index 839c47d..cb5a299 100644 --- a/src/commands/qstash/get.ts +++ b/src/commands/qstash/get.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { QStashUser } from "../../types.js"; export function registerQStashGet(qstash: Command): void { @@ -13,11 +13,7 @@ export function registerQStashGet(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const q = await request(auth, "GET", `/v2/qstash/user/${flags.qstashId}`); - printJSON(q); - } catch (err) { - handleError(err); - } + const q = await request(auth, "GET", `/v2/qstash/user/${flags.qstashId}`); + printJSON(q); }); } diff --git a/src/commands/qstash/ipv4.ts b/src/commands/qstash/ipv4.ts index cf032bf..327e7e3 100644 --- a/src/commands/qstash/ipv4.ts +++ b/src/commands/qstash/ipv4.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerQStashIpv4(qstash: Command): void { qstash @@ -11,11 +11,7 @@ export function registerQStashIpv4(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const addresses = await request(auth, "GET", "/v2/qstash/ipv4"); - printJSON(addresses); - } catch (err) { - handleError(err); - } + const addresses = await request(auth, "GET", "/v2/qstash/ipv4"); + printJSON(addresses); }); } diff --git a/src/commands/qstash/list.ts b/src/commands/qstash/list.ts index f280175..5ffa50a 100644 --- a/src/commands/qstash/list.ts +++ b/src/commands/qstash/list.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { QStashUser } from "../../types.js"; export function registerQStashList(qstash: Command): void { @@ -12,11 +12,7 @@ export function registerQStashList(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const users = await request(auth, "GET", "/v2/qstash/users"); - printJSON(users); - } catch (err) { - handleError(err); - } + const users = await request(auth, "GET", "/v2/qstash/users"); + printJSON(users); }); } diff --git a/src/commands/qstash/move-to-team.ts b/src/commands/qstash/move-to-team.ts index 993c3fa..c5249e4 100644 --- a/src/commands/qstash/move-to-team.ts +++ b/src/commands/qstash/move-to-team.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerQStashMoveToTeam(qstash: Command): void { qstash @@ -13,11 +13,7 @@ export function registerQStashMoveToTeam(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; qstashId: string; targetTeamId: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", "/v2/qstash/move-to-team", { qstash_id: flags.qstashId, target_team_id: flags.targetTeamId }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", "/v2/qstash/move-to-team", { qstash_id: flags.qstashId, target_team_id: flags.targetTeamId }); + printJSON(result); }); } diff --git a/src/commands/qstash/rotate-token.ts b/src/commands/qstash/rotate-token.ts index 2a7048d..1fac7c5 100644 --- a/src/commands/qstash/rotate-token.ts +++ b/src/commands/qstash/rotate-token.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { QStashUser } from "../../types.js"; export function registerQStashRotateToken(qstash: Command): void { @@ -13,11 +13,7 @@ export function registerQStashRotateToken(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const q = await request(auth, "POST", `/v2/qstash/rotate-token/${flags.qstashId}`); - printJSON(q); - } catch (err) { - handleError(err); - } + const q = await request(auth, "POST", `/v2/qstash/rotate-token/${flags.qstashId}`); + printJSON(q); }); } diff --git a/src/commands/qstash/set-plan.ts b/src/commands/qstash/set-plan.ts index e81f437..54dbf24 100644 --- a/src/commands/qstash/set-plan.ts +++ b/src/commands/qstash/set-plan.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { QSTASH_PLANS } from "../../types.js"; export function registerQStashSetPlan(qstash: Command): void { @@ -14,11 +14,7 @@ export function registerQStashSetPlan(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string; plan: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/qstash/set-plan/${flags.qstashId}`, { plan_name: flags.plan }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/qstash/set-plan/${flags.qstashId}`, { plan_name: flags.plan }); + printJSON(result); }); } diff --git a/src/commands/qstash/stats.ts b/src/commands/qstash/stats.ts index 965cade..bfda1ba 100644 --- a/src/commands/qstash/stats.ts +++ b/src/commands/qstash/stats.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { STATS_PERIODS } from "../../types.js"; export function registerQStashStats(qstash: Command): void { @@ -15,11 +15,7 @@ export function registerQStashStats(qstash: Command): void { .action(async (flags: { qstashId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; - try { - const stats = await request>(auth, "GET", `/v2/qstash/stats/${flags.qstashId}${qs}`); - printJSON(stats); - } catch (err) { - handleError(err); - } + const stats = await request>(auth, "GET", `/v2/qstash/stats/${flags.qstashId}${qs}`); + printJSON(stats); }); } diff --git a/src/commands/qstash/update-budget.ts b/src/commands/qstash/update-budget.ts index a90f15e..8e28df1 100644 --- a/src/commands/qstash/update-budget.ts +++ b/src/commands/qstash/update-budget.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerQStashUpdateBudget(qstash: Command): void { qstash @@ -13,14 +13,10 @@ export function registerQStashUpdateBudget(qstash: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { qstashId: string; email?: string; apiKey?: string; budget: number }) => { if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { - handleError(new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (dollars).`)); + throw new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (dollars).`); } const auth = resolveAuth(flags); - try { - const result = await request(auth, "PATCH", `/v2/qstash/update-budget/${flags.qstashId}`, { budget: flags.budget }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "PATCH", `/v2/qstash/update-budget/${flags.qstashId}`, { budget: flags.budget }); + printJSON(result); }); } diff --git a/src/commands/redis/backup/create.ts b/src/commands/redis/backup/create.ts index 0d87c05..5241986 100644 --- a/src/commands/redis/backup/create.ts +++ b/src/commands/redis/backup/create.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, handleError } from "../../../output.js"; +import { printJSON } from "../../../output.js"; export function registerBackupCreate(backup: Command): void { backup @@ -13,11 +13,7 @@ export function registerBackupCreate(backup: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; name: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/create-backup/${flags.dbId}`, { name: flags.name }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/create-backup/${flags.dbId}`, { name: flags.name }); + printJSON(result); }); } diff --git a/src/commands/redis/backup/delete.ts b/src/commands/redis/backup/delete.ts index 26a0f18..790fdcb 100644 --- a/src/commands/redis/backup/delete.ts +++ b/src/commands/redis/backup/delete.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, handleError } from "../../../output.js"; +import { printJSON } from "../../../output.js"; export function registerBackupDelete(backup: Command): void { backup @@ -18,11 +18,7 @@ export function registerBackupDelete(backup: Command): void { return; } const auth = resolveAuth(flags); - try { - await request(auth, "DELETE", `/v2/redis/delete-backup/${flags.dbId}/${flags.backupId}`); - printJSON({ deleted: true, backup_id: flags.backupId }); - } catch (err) { - handleError(err); - } + await request(auth, "DELETE", `/v2/redis/delete-backup/${flags.dbId}/${flags.backupId}`); + printJSON({ deleted: true, backup_id: flags.backupId }); }); } diff --git a/src/commands/redis/backup/disable-daily.ts b/src/commands/redis/backup/disable-daily.ts index 291a946..d5fa75b 100644 --- a/src/commands/redis/backup/disable-daily.ts +++ b/src/commands/redis/backup/disable-daily.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, handleError } from "../../../output.js"; +import { printJSON } from "../../../output.js"; export function registerDisableDaily(backup: Command): void { backup @@ -12,11 +12,7 @@ export function registerDisableDaily(backup: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/disable-dailybackup/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/disable-dailybackup/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/backup/enable-daily.ts b/src/commands/redis/backup/enable-daily.ts index bd1e332..faa833a 100644 --- a/src/commands/redis/backup/enable-daily.ts +++ b/src/commands/redis/backup/enable-daily.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, handleError } from "../../../output.js"; +import { printJSON } from "../../../output.js"; export function registerEnableDaily(backup: Command): void { backup @@ -12,11 +12,7 @@ export function registerEnableDaily(backup: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/enable-dailybackup/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/enable-dailybackup/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/backup/list.ts b/src/commands/redis/backup/list.ts index 9f4b226..733f70e 100644 --- a/src/commands/redis/backup/list.ts +++ b/src/commands/redis/backup/list.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, handleError } from "../../../output.js"; +import { printJSON } from "../../../output.js"; import type { Backup } from "../../../types.js"; export function registerBackupList(backup: Command): void { @@ -13,11 +13,7 @@ export function registerBackupList(backup: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const backups = await request(auth, "GET", `/v2/redis/list-backup/${flags.dbId}`); - printJSON(backups); - } catch (err) { - handleError(err); - } + const backups = await request(auth, "GET", `/v2/redis/list-backup/${flags.dbId}`); + printJSON(backups); }); } diff --git a/src/commands/redis/backup/restore.ts b/src/commands/redis/backup/restore.ts index b2b277a..7b67803 100644 --- a/src/commands/redis/backup/restore.ts +++ b/src/commands/redis/backup/restore.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../../auth.js"; import { request } from "../../../client.js"; -import { printJSON, handleError } from "../../../output.js"; +import { printJSON } from "../../../output.js"; export function registerBackupRestore(backup: Command): void { backup @@ -13,11 +13,7 @@ export function registerBackupRestore(backup: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; backupId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/restore-backup/${flags.dbId}`, { backup_id: flags.backupId }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/restore-backup/${flags.dbId}`, { backup_id: flags.backupId }); + printJSON(result); }); } diff --git a/src/commands/redis/change-plan.ts b/src/commands/redis/change-plan.ts index 72eef03..e8b22c9 100644 --- a/src/commands/redis/change-plan.ts +++ b/src/commands/redis/change-plan.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerChangePlan(redis: Command): void { redis @@ -13,11 +13,7 @@ export function registerChangePlan(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; plan: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/change-plan/${flags.dbId}`, { plan: flags.plan }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/change-plan/${flags.dbId}`, { plan: flags.plan }); + printJSON(result); }); } diff --git a/src/commands/redis/create.ts b/src/commands/redis/create.ts index bf9aea0..077705e 100644 --- a/src/commands/redis/create.ts +++ b/src/commands/redis/create.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { REGIONS } from "../../types.js"; import type { Database } from "../../types.js"; @@ -16,20 +16,15 @@ export function registerCreate(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; readRegions?: string[] }) => { if (!(REGIONS as readonly string[]).includes(flags.region)) { - console.error(JSON.stringify({ error: `Invalid region '${flags.region}'. Available: ${REGIONS.join(", ")}` })); - process.exit(1); + throw new Error(`Invalid region '${flags.region}'. Available: ${REGIONS.join(", ")}`); } const auth = resolveAuth(flags); - try { - const db = await request(auth, "POST", "/v2/redis/database", { - database_name: flags.name, - region: "global", - primary_region: flags.region, - read_regions: flags.readRegions, - }); - printJSON(db); - } catch (err) { - handleError(err); - } + const db = await request(auth, "POST", "/v2/redis/database", { + database_name: flags.name, + region: "global", + primary_region: flags.region, + read_regions: flags.readRegions, + }); + printJSON(db); }); } diff --git a/src/commands/redis/delete.ts b/src/commands/redis/delete.ts index 3e3f5b0..f8bfbb1 100644 --- a/src/commands/redis/delete.ts +++ b/src/commands/redis/delete.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerDelete(redis: Command): void { redis @@ -17,11 +17,7 @@ export function registerDelete(redis: Command): void { return; } const auth = resolveAuth(flags); - try { - await request(auth, "DELETE", `/v2/redis/database/${flags.dbId}`); - printJSON({ deleted: true, database_id: flags.dbId }); - } catch (err) { - handleError(err); - } + await request(auth, "DELETE", `/v2/redis/database/${flags.dbId}`); + printJSON({ deleted: true, database_id: flags.dbId }); }); } diff --git a/src/commands/redis/disable-autoupgrade.ts b/src/commands/redis/disable-autoupgrade.ts index 1f233d0..d86685d 100644 --- a/src/commands/redis/disable-autoupgrade.ts +++ b/src/commands/redis/disable-autoupgrade.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerDisableAutoupgrade(redis: Command): void { redis @@ -12,11 +12,7 @@ export function registerDisableAutoupgrade(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/disable-autoupgrade/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/disable-autoupgrade/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/disable-eviction.ts b/src/commands/redis/disable-eviction.ts index 6c00554..213dd1e 100644 --- a/src/commands/redis/disable-eviction.ts +++ b/src/commands/redis/disable-eviction.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerDisableEviction(redis: Command): void { redis @@ -12,11 +12,7 @@ export function registerDisableEviction(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/disable-eviction/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/disable-eviction/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/enable-autoupgrade.ts b/src/commands/redis/enable-autoupgrade.ts index da3cff7..16554ef 100644 --- a/src/commands/redis/enable-autoupgrade.ts +++ b/src/commands/redis/enable-autoupgrade.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerEnableAutoupgrade(redis: Command): void { redis @@ -12,11 +12,7 @@ export function registerEnableAutoupgrade(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/enable-autoupgrade/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/enable-autoupgrade/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/enable-eviction.ts b/src/commands/redis/enable-eviction.ts index 2aaef31..606c20e 100644 --- a/src/commands/redis/enable-eviction.ts +++ b/src/commands/redis/enable-eviction.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerEnableEviction(redis: Command): void { redis @@ -12,11 +12,7 @@ export function registerEnableEviction(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/enable-eviction/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/enable-eviction/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/enable-tls.ts b/src/commands/redis/enable-tls.ts index 3d7c675..6e8e129 100644 --- a/src/commands/redis/enable-tls.ts +++ b/src/commands/redis/enable-tls.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerEnableTls(redis: Command): void { redis @@ -12,11 +12,7 @@ export function registerEnableTls(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/enable-tls/${flags.dbId}`); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/enable-tls/${flags.dbId}`); + printJSON(result); }); } diff --git a/src/commands/redis/exec.ts b/src/commands/redis/exec.ts index 2ae0d41..aac36b8 100644 --- a/src/commands/redis/exec.ts +++ b/src/commands/redis/exec.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; interface Flags { dbUrl: string; @@ -17,31 +17,26 @@ export function registerExec(redis: Command): void { .action(async (flags: Flags) => { const args = parseCommand(flags.command); if (args.length === 0) { - handleError(new Error("Empty command")); + throw new Error("Empty command"); } - try { - const url = flags.dbUrl.replace(/\/$/, ""); - const response = await fetch(url, { - method: "POST", - headers: { - Authorization: `Bearer ${flags.dbToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(args), - }); + const url = flags.dbUrl.replace(/\/$/, ""); + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${flags.dbToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(args), + }); - const data = await response.json() as { result?: unknown; error?: string }; + const data = await response.json() as { result?: unknown; error?: string }; - if (data.error) { - console.error(JSON.stringify({ error: data.error })); - process.exit(1); - } - - printJSON({ result: data.result }); - } catch (err) { - handleError(err); + if (data.error) { + throw new Error(data.error); } + + printJSON({ result: data.result }); }); } diff --git a/src/commands/redis/get.ts b/src/commands/redis/get.ts index ee407ba..b1962c9 100644 --- a/src/commands/redis/get.ts +++ b/src/commands/redis/get.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { Database } from "../../types.js"; export function registerGet(redis: Command): void { @@ -15,11 +15,7 @@ export function registerGet(redis: Command): void { .action(async (flags: { dbId: string; hideCredentials?: boolean; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); const qs = flags.hideCredentials ? "?credentials=hide" : ""; - try { - const db = await request(auth, "GET", `/v2/redis/database/${flags.dbId}${qs}`); - printJSON(db); - } catch (err) { - handleError(err); - } + const db = await request(auth, "GET", `/v2/redis/database/${flags.dbId}${qs}`); + printJSON(db); }); } diff --git a/src/commands/redis/list.ts b/src/commands/redis/list.ts index 2a6c994..823f228 100644 --- a/src/commands/redis/list.ts +++ b/src/commands/redis/list.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { Database } from "../../types.js"; export function registerList(redis: Command): void { @@ -12,11 +12,7 @@ export function registerList(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const dbs = await request(auth, "GET", "/v2/redis/databases"); - printJSON(dbs); - } catch (err) { - handleError(err); - } + const dbs = await request(auth, "GET", "/v2/redis/databases"); + printJSON(dbs); }); } diff --git a/src/commands/redis/move-to-team.ts b/src/commands/redis/move-to-team.ts index d6fdfdb..702b56b 100644 --- a/src/commands/redis/move-to-team.ts +++ b/src/commands/redis/move-to-team.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerMoveToTeam(redis: Command): void { redis @@ -13,11 +13,7 @@ export function registerMoveToTeam(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; teamId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/move-to-team`, { database_id: flags.dbId, team_id: flags.teamId }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/move-to-team`, { database_id: flags.dbId, team_id: flags.teamId }); + printJSON(result); }); } diff --git a/src/commands/redis/rename.ts b/src/commands/redis/rename.ts index 63a94e0..9c721b0 100644 --- a/src/commands/redis/rename.ts +++ b/src/commands/redis/rename.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { Database } from "../../types.js"; export function registerRename(redis: Command): void { @@ -14,11 +14,7 @@ export function registerRename(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; name: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const db = await request(auth, "POST", `/v2/redis/rename/${flags.dbId}`, { name: flags.name }); - printJSON(db); - } catch (err) { - handleError(err); - } + const db = await request(auth, "POST", `/v2/redis/rename/${flags.dbId}`, { name: flags.name }); + printJSON(db); }); } diff --git a/src/commands/redis/reset-password.ts b/src/commands/redis/reset-password.ts index 158c7a1..a43aee6 100644 --- a/src/commands/redis/reset-password.ts +++ b/src/commands/redis/reset-password.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { Database } from "../../types.js"; export function registerResetPassword(redis: Command): void { @@ -13,11 +13,7 @@ export function registerResetPassword(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const db = await request(auth, "POST", `/v2/redis/reset-password/${flags.dbId}`); - printJSON(db); - } catch (err) { - handleError(err); - } + const db = await request(auth, "POST", `/v2/redis/reset-password/${flags.dbId}`); + printJSON(db); }); } diff --git a/src/commands/redis/stats.ts b/src/commands/redis/stats.ts index 6db3f48..e49316d 100644 --- a/src/commands/redis/stats.ts +++ b/src/commands/redis/stats.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerStats(redis: Command): void { redis @@ -12,11 +12,7 @@ export function registerStats(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const stats = await request>(auth, "GET", `/v2/redis/stats/${flags.dbId}`); - printJSON(stats); - } catch (err) { - handleError(err); - } + const stats = await request>(auth, "GET", `/v2/redis/stats/${flags.dbId}`); + printJSON(stats); }); } diff --git a/src/commands/redis/update-budget.ts b/src/commands/redis/update-budget.ts index 2315c87..ab2f365 100644 --- a/src/commands/redis/update-budget.ts +++ b/src/commands/redis/update-budget.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerUpdateBudget(redis: Command): void { redis @@ -13,14 +13,10 @@ export function registerUpdateBudget(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; budget: number; email?: string; apiKey?: string }) => { if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { - handleError(new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (cents).`)); + throw new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (cents).`); } const auth = resolveAuth(flags); - try { - const result = await request(auth, "PATCH", `/v2/redis/update-budget/${flags.dbId}`, { budget: flags.budget }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "PATCH", `/v2/redis/update-budget/${flags.dbId}`, { budget: flags.budget }); + printJSON(result); }); } diff --git a/src/commands/redis/update-regions.ts b/src/commands/redis/update-regions.ts index e726bec..17c13c9 100644 --- a/src/commands/redis/update-regions.ts +++ b/src/commands/redis/update-regions.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { REGIONS } from "../../types.js"; export function registerUpdateRegions(redis: Command): void { @@ -14,11 +14,7 @@ export function registerUpdateRegions(redis: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { dbId: string; readRegions: string[]; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/redis/update-regions/${flags.dbId}`, { read_regions: flags.readRegions }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/redis/update-regions/${flags.dbId}`, { read_regions: flags.readRegions }); + printJSON(result); }); } diff --git a/src/commands/search/create.ts b/src/commands/search/create.ts index f58c290..61370ce 100644 --- a/src/commands/search/create.ts +++ b/src/commands/search/create.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { SEARCH_REGIONS, SEARCH_PLANS } from "../../types.js"; import type { SearchIndex } from "../../types.js"; @@ -16,11 +16,7 @@ export function registerSearchCreate(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; type: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "POST", "/v2/search", { name: flags.name, region: flags.region, type: flags.type }); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "POST", "/v2/search", { name: flags.name, region: flags.region, type: flags.type }); + printJSON(idx); }); } diff --git a/src/commands/search/delete.ts b/src/commands/search/delete.ts index f79ea64..8b6fa52 100644 --- a/src/commands/search/delete.ts +++ b/src/commands/search/delete.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerSearchDelete(search: Command): void { search @@ -17,11 +17,7 @@ export function registerSearchDelete(search: Command): void { return; } const auth = resolveAuth(flags); - try { - await request(auth, "DELETE", `/v2/search/${flags.indexId}`); - printJSON({ deleted: true, index_id: flags.indexId }); - } catch (err) { - handleError(err); - } + await request(auth, "DELETE", `/v2/search/${flags.indexId}`); + printJSON({ deleted: true, index_id: flags.indexId }); }); } diff --git a/src/commands/search/get.ts b/src/commands/search/get.ts index 371b698..b44a80a 100644 --- a/src/commands/search/get.ts +++ b/src/commands/search/get.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { SearchIndex } from "../../types.js"; export function registerSearchGet(search: Command): void { @@ -13,11 +13,7 @@ export function registerSearchGet(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "GET", `/v2/search/${flags.indexId}`); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "GET", `/v2/search/${flags.indexId}`); + printJSON(idx); }); } diff --git a/src/commands/search/list.ts b/src/commands/search/list.ts index 9ef477d..e7f0300 100644 --- a/src/commands/search/list.ts +++ b/src/commands/search/list.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { SearchIndex } from "../../types.js"; export function registerSearchList(search: Command): void { @@ -12,11 +12,7 @@ export function registerSearchList(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const indexes = await request(auth, "GET", "/v2/search"); - printJSON(indexes); - } catch (err) { - handleError(err); - } + const indexes = await request(auth, "GET", "/v2/search"); + printJSON(indexes); }); } diff --git a/src/commands/search/rename.ts b/src/commands/search/rename.ts index 9499a6c..0c994ab 100644 --- a/src/commands/search/rename.ts +++ b/src/commands/search/rename.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { SearchIndex } from "../../types.js"; export function registerSearchRename(search: Command): void { @@ -14,11 +14,7 @@ export function registerSearchRename(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; name: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/rename`, { name: flags.name }); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/rename`, { name: flags.name }); + printJSON(idx); }); } diff --git a/src/commands/search/reset-password.ts b/src/commands/search/reset-password.ts index 1a9ce60..a5fae70 100644 --- a/src/commands/search/reset-password.ts +++ b/src/commands/search/reset-password.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { SearchIndex } from "../../types.js"; export function registerSearchResetPassword(search: Command): void { @@ -13,11 +13,7 @@ export function registerSearchResetPassword(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/reset-password`); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/reset-password`); + printJSON(idx); }); } diff --git a/src/commands/search/stats.ts b/src/commands/search/stats.ts index f6c1fbf..448296a 100644 --- a/src/commands/search/stats.ts +++ b/src/commands/search/stats.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { STATS_PERIODS } from "../../types.js"; export function registerSearchStats(search: Command): void { @@ -12,12 +12,8 @@ export function registerSearchStats(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const stats = await request>(auth, "GET", "/v2/search/stats"); - printJSON(stats); - } catch (err) { - handleError(err); - } + const stats = await request>(auth, "GET", "/v2/search/stats"); + printJSON(stats); }); search @@ -30,11 +26,7 @@ export function registerSearchStats(search: Command): void { .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; - try { - const stats = await request>(auth, "GET", `/v2/search/${flags.indexId}/stats${qs}`); - printJSON(stats); - } catch (err) { - handleError(err); - } + const stats = await request>(auth, "GET", `/v2/search/${flags.indexId}/stats${qs}`); + printJSON(stats); }); } diff --git a/src/commands/search/transfer.ts b/src/commands/search/transfer.ts index 7df0667..ccb9e6c 100644 --- a/src/commands/search/transfer.ts +++ b/src/commands/search/transfer.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerSearchTransfer(search: Command): void { search @@ -13,11 +13,7 @@ export function registerSearchTransfer(search: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; targetAccount: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/search/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/search/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); + printJSON(result); }); } diff --git a/src/commands/team/add-member.ts b/src/commands/team/add-member.ts index 947b07f..e72abf8 100644 --- a/src/commands/team/add-member.ts +++ b/src/commands/team/add-member.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { TEAM_MEMBER_ROLES } from "../../types.js"; import type { TeamMember } from "../../types.js"; @@ -16,18 +16,14 @@ export function registerTeamAddMember(team: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; teamId: string; memberEmail: string; role: string }) => { if (!(TEAM_MEMBER_ROLES as readonly string[]).includes(flags.role)) { - handleError(new Error(`Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`)); + throw new Error(`Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`); } const auth = resolveAuth(flags); - try { - const member = await request(auth, "POST", "/v2/teams/member", { - team_id: flags.teamId, - member_email: flags.memberEmail, - member_role: flags.role, - }); - printJSON(member); - } catch (err) { - handleError(err); - } + const member = await request(auth, "POST", "/v2/teams/member", { + team_id: flags.teamId, + member_email: flags.memberEmail, + member_role: flags.role, + }); + printJSON(member); }); } diff --git a/src/commands/team/create.ts b/src/commands/team/create.ts index 3f8ef81..4697eb0 100644 --- a/src/commands/team/create.ts +++ b/src/commands/team/create.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { Team } from "../../types.js"; export function registerTeamCreate(team: Command): void { @@ -14,11 +14,7 @@ export function registerTeamCreate(team: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; name: string; copyCc?: boolean }) => { const auth = resolveAuth(flags); - try { - const t = await request(auth, "POST", "/v2/team", { team_name: flags.name, copy_cc: flags.copyCc ?? false }); - printJSON(t); - } catch (err) { - handleError(err); - } + const t = await request(auth, "POST", "/v2/team", { team_name: flags.name, copy_cc: flags.copyCc ?? false }); + printJSON(t); }); } diff --git a/src/commands/team/delete.ts b/src/commands/team/delete.ts index 3188cee..a0f1d29 100644 --- a/src/commands/team/delete.ts +++ b/src/commands/team/delete.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerTeamDelete(team: Command): void { team @@ -17,11 +17,7 @@ export function registerTeamDelete(team: Command): void { return; } const auth = resolveAuth(flags); - try { - await request(auth, "DELETE", `/v2/team/${flags.teamId}`); - printJSON({ deleted: true, team_id: flags.teamId }); - } catch (err) { - handleError(err); - } + await request(auth, "DELETE", `/v2/team/${flags.teamId}`); + printJSON({ deleted: true, team_id: flags.teamId }); }); } diff --git a/src/commands/team/list.ts b/src/commands/team/list.ts index 617598c..b84e090 100644 --- a/src/commands/team/list.ts +++ b/src/commands/team/list.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { Team } from "../../types.js"; export function registerTeamList(team: Command): void { @@ -12,11 +12,7 @@ export function registerTeamList(team: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const teams = await request(auth, "GET", "/v2/teams"); - printJSON(teams); - } catch (err) { - handleError(err); - } + const teams = await request(auth, "GET", "/v2/teams"); + printJSON(teams); }); } diff --git a/src/commands/team/members.ts b/src/commands/team/members.ts index 4541ed7..1dc8ae4 100644 --- a/src/commands/team/members.ts +++ b/src/commands/team/members.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { TeamMember } from "../../types.js"; export function registerTeamMembers(team: Command): void { @@ -13,11 +13,7 @@ export function registerTeamMembers(team: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { teamId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const members = await request(auth, "GET", `/v2/teams/${flags.teamId}`); - printJSON(members); - } catch (err) { - handleError(err); - } + const members = await request(auth, "GET", `/v2/teams/${flags.teamId}`); + printJSON(members); }); } diff --git a/src/commands/team/remove-member.ts b/src/commands/team/remove-member.ts index f1b0b2c..1e8f449 100644 --- a/src/commands/team/remove-member.ts +++ b/src/commands/team/remove-member.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerTeamRemoveMember(team: Command): void { team @@ -18,11 +18,7 @@ export function registerTeamRemoveMember(team: Command): void { return; } const auth = resolveAuth(flags); - try { - await request(auth, "DELETE", "/v2/teams/member", { team_id: flags.teamId, member_email: flags.memberEmail }); - printJSON({ removed: true, team_id: flags.teamId, member_email: flags.memberEmail }); - } catch (err) { - handleError(err); - } + await request(auth, "DELETE", "/v2/teams/member", { team_id: flags.teamId, member_email: flags.memberEmail }); + printJSON({ removed: true, team_id: flags.teamId, member_email: flags.memberEmail }); }); } diff --git a/src/commands/vector/create.ts b/src/commands/vector/create.ts index 32295b6..ef54628 100644 --- a/src/commands/vector/create.ts +++ b/src/commands/vector/create.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { VECTOR_REGIONS, VECTOR_SIMILARITY_FUNCTIONS, VECTOR_INDEX_TYPES, VECTOR_EMBEDDING_MODELS, VECTOR_SPARSE_MODELS, VECTOR_PLANS } from "../../types.js"; import type { VectorIndex } from "../../types.js"; @@ -21,23 +21,19 @@ export function registerVectorCreate(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; similarityFunction: string; dimensionCount: number; type?: string; embeddingModel?: string; indexType?: string; sparseEmbeddingModel?: string }) => { if (!Number.isFinite(flags.dimensionCount) || !Number.isInteger(flags.dimensionCount) || flags.dimensionCount < 0) { - handleError(new Error(`Invalid --dimension-count: "${flags.dimensionCount}". Must be a non-negative integer.`)); + throw new Error(`Invalid --dimension-count: "${flags.dimensionCount}". Must be a non-negative integer.`); } const auth = resolveAuth(flags); - try { - const idx = await request(auth, "POST", "/v2/vector/index", { - name: flags.name, - region: flags.region, - similarity_function: flags.similarityFunction, - dimension_count: flags.dimensionCount, - type: flags.type, - embedding_model: flags.embeddingModel, - index_type: flags.indexType, - sparse_embedding_model: flags.sparseEmbeddingModel, - }); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "POST", "/v2/vector/index", { + name: flags.name, + region: flags.region, + similarity_function: flags.similarityFunction, + dimension_count: flags.dimensionCount, + type: flags.type, + embedding_model: flags.embeddingModel, + index_type: flags.indexType, + sparse_embedding_model: flags.sparseEmbeddingModel, + }); + printJSON(idx); }); } diff --git a/src/commands/vector/delete.ts b/src/commands/vector/delete.ts index 1026562..3904f69 100644 --- a/src/commands/vector/delete.ts +++ b/src/commands/vector/delete.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerVectorDelete(vector: Command): void { vector @@ -17,11 +17,7 @@ export function registerVectorDelete(vector: Command): void { return; } const auth = resolveAuth(flags); - try { - await request(auth, "DELETE", `/v2/vector/index/${flags.indexId}`); - printJSON({ deleted: true, index_id: flags.indexId }); - } catch (err) { - handleError(err); - } + await request(auth, "DELETE", `/v2/vector/index/${flags.indexId}`); + printJSON({ deleted: true, index_id: flags.indexId }); }); } diff --git a/src/commands/vector/get.ts b/src/commands/vector/get.ts index 1486c5a..1487cc2 100644 --- a/src/commands/vector/get.ts +++ b/src/commands/vector/get.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { VectorIndex } from "../../types.js"; export function registerVectorGet(vector: Command): void { @@ -13,11 +13,7 @@ export function registerVectorGet(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "GET", `/v2/vector/index/${flags.indexId}`); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "GET", `/v2/vector/index/${flags.indexId}`); + printJSON(idx); }); } diff --git a/src/commands/vector/list.ts b/src/commands/vector/list.ts index 666bf3d..59a3e61 100644 --- a/src/commands/vector/list.ts +++ b/src/commands/vector/list.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { VectorIndex } from "../../types.js"; export function registerVectorList(vector: Command): void { @@ -12,11 +12,7 @@ export function registerVectorList(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const indexes = await request(auth, "GET", "/v2/vector/index"); - printJSON(indexes); - } catch (err) { - handleError(err); - } + const indexes = await request(auth, "GET", "/v2/vector/index"); + printJSON(indexes); }); } diff --git a/src/commands/vector/rename.ts b/src/commands/vector/rename.ts index 494fc34..23adb3d 100644 --- a/src/commands/vector/rename.ts +++ b/src/commands/vector/rename.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { VectorIndex } from "../../types.js"; export function registerVectorRename(vector: Command): void { @@ -14,11 +14,7 @@ export function registerVectorRename(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; name: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/rename`, { name: flags.name }); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/rename`, { name: flags.name }); + printJSON(idx); }); } diff --git a/src/commands/vector/reset-password.ts b/src/commands/vector/reset-password.ts index c64e0a2..a9c1ab0 100644 --- a/src/commands/vector/reset-password.ts +++ b/src/commands/vector/reset-password.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import type { VectorIndex } from "../../types.js"; export function registerVectorResetPassword(vector: Command): void { @@ -13,11 +13,7 @@ export function registerVectorResetPassword(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/reset-password`); - printJSON(idx); - } catch (err) { - handleError(err); - } + const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/reset-password`); + printJSON(idx); }); } diff --git a/src/commands/vector/set-plan.ts b/src/commands/vector/set-plan.ts index 7de6acc..2bcd523 100644 --- a/src/commands/vector/set-plan.ts +++ b/src/commands/vector/set-plan.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { VECTOR_PLANS } from "../../types.js"; export function registerVectorSetPlan(vector: Command): void { @@ -14,11 +14,7 @@ export function registerVectorSetPlan(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; plan: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/setplan`, { target_plan: flags.plan }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/setplan`, { target_plan: flags.plan }); + printJSON(result); }); } diff --git a/src/commands/vector/stats.ts b/src/commands/vector/stats.ts index 0d4cde4..6d306af 100644 --- a/src/commands/vector/stats.ts +++ b/src/commands/vector/stats.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; import { STATS_PERIODS } from "../../types.js"; export function registerVectorStats(vector: Command): void { @@ -12,12 +12,8 @@ export function registerVectorStats(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { email?: string; apiKey?: string }) => { const auth = resolveAuth(flags); - try { - const stats = await request>(auth, "GET", "/v2/vector/index/stats"); - printJSON(stats); - } catch (err) { - handleError(err); - } + const stats = await request>(auth, "GET", "/v2/vector/index/stats"); + printJSON(stats); }); vector @@ -30,11 +26,7 @@ export function registerVectorStats(vector: Command): void { .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { const auth = resolveAuth(flags); const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; - try { - const stats = await request>(auth, "GET", `/v2/vector/index/${flags.indexId}/stats${qs}`); - printJSON(stats); - } catch (err) { - handleError(err); - } + const stats = await request>(auth, "GET", `/v2/vector/index/${flags.indexId}/stats${qs}`); + printJSON(stats); }); } diff --git a/src/commands/vector/transfer.ts b/src/commands/vector/transfer.ts index 559930c..cc8b034 100644 --- a/src/commands/vector/transfer.ts +++ b/src/commands/vector/transfer.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; -import { printJSON, handleError } from "../../output.js"; +import { printJSON } from "../../output.js"; export function registerVectorTransfer(vector: Command): void { vector @@ -13,11 +13,7 @@ export function registerVectorTransfer(vector: Command): void { .option("--api-key ", "Upstash API key") .action(async (flags: { indexId: string; email?: string; apiKey?: string; targetAccount: string }) => { const auth = resolveAuth(flags); - try { - const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); - printJSON(result); - } catch (err) { - handleError(err); - } + const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); + printJSON(result); }); } From fdb9b62f50508e94a65086abb94c9b1794e3009c Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Tue, 14 Apr 2026 17:53:07 +0300 Subject: [PATCH 07/18] feat: add dotenv support --- .agents/skills/upstash-cli/SKILL.md | 18 ++++++++++++++++-- CLAUDE.md | 18 ++++++++++++++++-- README.md | 20 +++++++++++++++++--- package-lock.json | 15 ++++++++++++++- package.json | 12 +++++++++--- src/auth.ts | 5 ++--- src/cli.ts | 14 +++++++++++++- src/commands/redis/exec.ts | 21 +++++++++++++++------ 8 files changed, 102 insertions(+), 21 deletions(-) diff --git a/.agents/skills/upstash-cli/SKILL.md b/.agents/skills/upstash-cli/SKILL.md index 7aeeb28..56dd383 100644 --- a/.agents/skills/upstash-cli/SKILL.md +++ b/.agents/skills/upstash-cli/SKILL.md @@ -15,13 +15,27 @@ From a clone (contributors / unreleased fixes): `npm install`, `npm run build`, ## Authentication -Set environment variables (recommended for agents): +Three ways to supply credentials (precedence: flags > env vars > `.env` file): +**Environment variables** (recommended for agents): ```bash export UPSTASH_EMAIL=you@example.com export UPSTASH_API_KEY=your_api_key ``` +**`.env` file** — place a `.env` in the working directory and the CLI loads it automatically: +```bash +UPSTASH_EMAIL=you@example.com +UPSTASH_API_KEY=your_api_key +``` + +**Per-command flags:** `--email ` and `--api-key ` override everything else for that invocation. + +**Custom `.env` path:** pass `--env-file ` as a global flag to load credentials from a specific file: +```bash +upstash --env-file ~/secrets/.env redis list +``` + **Agents:** Prefer a **read-only** Developer API key in `UPSTASH_API_KEY` when you can. The API only returns what that key may access, and only actions permitted for read-only keys succeed; the rest fail at the API. ## Global Flags @@ -78,7 +92,7 @@ upstash redis exec --db-url --db-token --command "GET key" upstash redis exec --db-url --db-token --command "HGETALL myhash" ``` -The `--db-url` and `--db-token` come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. +`--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). The values come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. ### Core CRUD diff --git a/CLAUDE.md b/CLAUDE.md index 92b1b3e..798909b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,13 +15,27 @@ From a clone: `npm install`, `npm run build`, then `node dist/cli.js …` or `np ## Authentication -Set environment variables before running any command: +Three ways to supply credentials (precedence: flags > env vars > `.env` file): +**Environment variables:** ```bash export UPSTASH_EMAIL=you@example.com export UPSTASH_API_KEY=your_api_key ``` +**`.env` file** — place a `.env` in the working directory and the CLI loads it automatically: +```bash +UPSTASH_EMAIL=you@example.com +UPSTASH_API_KEY=your_api_key +``` + +**Per-command flags:** `--email ` and `--api-key ` override everything else for that invocation. + +**Custom `.env` path:** pass `--env-file ` as a global flag to load credentials from a specific file: +```bash +upstash --env-file ~/secrets/.env redis list +``` + **Agents:** You can use a **read-only** Developer API key in `UPSTASH_API_KEY`. The Developer API then only returns what that key is allowed to see, and only the actions allowed for that key will succeed; anything else fails at the API. ## Global Flags @@ -78,7 +92,7 @@ upstash redis exec --db-url --db-token --command "GET key" upstash redis exec --db-url --db-token --command "HGETALL myhash" ``` -The `--db-url` and `--db-token` come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. +`--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). The values come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. ### Core CRUD diff --git a/README.md b/README.md index 0c6d12f..0394afe 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,28 @@ Prebuilt binaries (Windows, Linux, macOS Intel and Apple Silicon) are on [GitHub ## Authentication -Set both variables before running commands: +Pick any one of these three methods: +**Environment variables** ```bash export UPSTASH_EMAIL=you@example.com export UPSTASH_API_KEY=your_api_key ``` -Most commands also accept `--email` and `--api-key`, which override the environment for that invocation. +**`.env` file** — place a `.env` in your working directory and the CLI loads it automatically: +```bash +UPSTASH_EMAIL=you@example.com +UPSTASH_API_KEY=your_api_key +``` + +**Per-command flags** — `--email` and `--api-key` override everything else for that invocation. + +**Custom `.env` path** — use `--env-file ` to load a file from a specific location: +```bash +upstash --env-file ~/secrets/.env redis list +``` + +Precedence: flags > environment variables > `.env` file. For agents, a **read-only** Developer API key is often enough: the API only returns what that key allows, and only those operations succeed—mutations fail at the API like in the console. @@ -74,7 +88,7 @@ upstash redis exec --db-url --db-token --command "SET key value" upstash redis exec --db-url --db-token --command "GET key" ``` -Use `endpoint` / `https://...` and `rest_token` from `upstash redis get --db-id `. +`--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). Use `endpoint` and `rest_token` from `upstash redis get --db-id `. ### Core diff --git a/package-lock.json b/package-lock.json index 4c272ab..bf221be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "commander": "^13.0.0" + "commander": "^13.0.0", + "dotenv": "^17.4.2" }, "bin": { "upstash": "dist/cli.js" @@ -985,6 +986,18 @@ "node": ">=6" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", diff --git a/package.json b/package.json index 77f2ed8..48d56e1 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,15 @@ "test:watch": "vitest", "typecheck": "tsc -p tsconfig.test.json --noEmit" }, - "keywords": ["upstash", "cli"], + "keywords": [ + "upstash", + "cli" + ], "author": "Upstash", "license": "MIT", "dependencies": { - "commander": "^13.0.0" + "commander": "^13.0.0", + "dotenv": "^17.4.2" }, "devDependencies": { "@types/node": "^20.10.0", @@ -35,5 +39,7 @@ "publishConfig": { "access": "public" }, - "files": ["dist"] + "files": [ + "dist" + ] } diff --git a/src/auth.ts b/src/auth.ts index 9bf2993..cfa450b 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -8,10 +8,9 @@ export function resolveAuth(flags: { email?: string; apiKey?: string }): Auth { const apiKey = flags.apiKey ?? process.env.UPSTASH_API_KEY; if (!email || !apiKey) { - console.error( - JSON.stringify({ error: "Authentication required. Provide credentials via --email and --api-key flags or set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables." }), + throw new Error( + "Authentication required. Provide credentials via --email and --api-key flags, set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables, or add them to a .env file in the current directory." ); - process.exit(1); } return { email, apiKey }; diff --git a/src/cli.ts b/src/cli.ts index 231e776..ee71576 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -8,13 +8,25 @@ import { registerVector } from "./commands/vector/index.js"; import { registerSearch } from "./commands/search/index.js"; import { registerQStash } from "./commands/qstash/index.js"; import { handleError } from "./output.js"; +import dotenv from "dotenv"; + +// Pre-scan argv for --env-file before Commander parses, so dotenv loads +// the right file before any command action reads process.env. +const envFileIndex = process.argv.indexOf("--env-file"); +const envFilePath = envFileIndex !== -1 ? process.argv[envFileIndex + 1] : undefined; +const dotenvResult = dotenv.config(envFilePath ? { path: envFilePath } : undefined); +if (envFilePath && dotenvResult.error) { + console.error(JSON.stringify({ error: `Could not load env file: ${envFilePath}` })); + process.exit(1); +} const program = new Command(); program .name("upstash") .description("Agent-friendly CLI for Upstash") - .version(version); + .version(version) + .option("--env-file ", "Path to a .env file to load credentials from"); registerRedis(program); registerTeam(program); diff --git a/src/commands/redis/exec.ts b/src/commands/redis/exec.ts index aac36b8..9e8799e 100644 --- a/src/commands/redis/exec.ts +++ b/src/commands/redis/exec.ts @@ -2,8 +2,8 @@ import { Command } from "commander"; import { printJSON } from "../../output.js"; interface Flags { - dbUrl: string; - dbToken: string; + dbUrl?: string; + dbToken?: string; command: string; } @@ -11,20 +11,29 @@ export function registerExec(redis: Command): void { redis .command("exec") .description("Execute a Redis command against a database via the REST API") - .requiredOption("--db-url ", "Database REST URL (e.g. https://xxx.upstash.io)") - .requiredOption("--db-token ", "Database REST token") + .option("--db-url ", "Database REST URL (overrides UPSTASH_REDIS_REST_URL)") + .option("--db-token ", "Database REST token (overrides UPSTASH_REDIS_REST_TOKEN)") .requiredOption("--command ", 'Redis command to run (e.g. "SET key value")') .action(async (flags: Flags) => { + const dbUrl = flags.dbUrl ?? process.env.UPSTASH_REDIS_REST_URL; + const dbToken = flags.dbToken ?? process.env.UPSTASH_REDIS_REST_TOKEN; + + if (!dbUrl || !dbToken) { + throw new Error( + "Database credentials required. Provide --db-url and --db-token or set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables." + ); + } + const args = parseCommand(flags.command); if (args.length === 0) { throw new Error("Empty command"); } - const url = flags.dbUrl.replace(/\/$/, ""); + const url = dbUrl.replace(/\/$/, ""); const response = await fetch(url, { method: "POST", headers: { - Authorization: `Bearer ${flags.dbToken}`, + Authorization: `Bearer ${dbToken}`, "Content-Type": "application/json", }, body: JSON.stringify(args), From cce0d53593f51f41e0eb695fd42760a20934133b Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Tue, 14 Apr 2026 17:57:21 +0300 Subject: [PATCH 08/18] feat: add ci.yml --- .github/workflows/ci.yml | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9dacaaf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Build & Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - run: npm ci + + - run: npm run build + + - run: npm run typecheck + + test: + name: Tests + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - run: npm ci + + - name: Run tests + env: + UPSTASH_EMAIL: ${{ secrets.UPSTASH_EMAIL }} + UPSTASH_API_KEY: ${{ secrets.UPSTASH_API_KEY }} + run: npm test From 76099fd7a403c04b7939fb9954ded2b4d32d6fe9 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:09:56 +0300 Subject: [PATCH 09/18] refactor: simplify redis exec to positional args or --json Drop the --command flag and custom shell-quote parser. Args are now either positional tokens (shell handles quoting) or a JSON array via --json. --- .agents/skills/upstash-cli/SKILL.md | 7 ++-- CLAUDE.md | 7 ++-- README.md | 5 +-- src/commands/redis/exec.ts | 55 +++++++++++++---------------- tests/integration/redis.test.ts | 8 ++--- tests/unit/parseCommand.test.ts | 40 --------------------- 6 files changed, 39 insertions(+), 83 deletions(-) delete mode 100644 tests/unit/parseCommand.test.ts diff --git a/.agents/skills/upstash-cli/SKILL.md b/.agents/skills/upstash-cli/SKILL.md index 56dd383..94771d0 100644 --- a/.agents/skills/upstash-cli/SKILL.md +++ b/.agents/skills/upstash-cli/SKILL.md @@ -87,9 +87,10 @@ All commands output JSON to stdout. Errors output `{ "error": "..." }` to stderr ### Execute Commands ```bash -upstash redis exec --db-url --db-token --command "SET key value" -upstash redis exec --db-url --db-token --command "GET key" -upstash redis exec --db-url --db-token --command "HGETALL myhash" +upstash redis exec --db-url --db-token SET key value +upstash redis exec --db-url --db-token GET key +upstash redis exec --db-url --db-token HGETALL myhash +upstash redis exec --db-url --db-token --json '["SET","key","value"]' ``` `--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). The values come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. diff --git a/CLAUDE.md b/CLAUDE.md index 798909b..9a4ea70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,9 +87,10 @@ All commands output JSON to stdout. Errors output `{ "error": "..." }` to stderr ### Execute Commands ```bash -upstash redis exec --db-url --db-token --command "SET key value" -upstash redis exec --db-url --db-token --command "GET key" -upstash redis exec --db-url --db-token --command "HGETALL myhash" +upstash redis exec --db-url --db-token SET key value +upstash redis exec --db-url --db-token GET key +upstash redis exec --db-url --db-token HGETALL myhash +upstash redis exec --db-url --db-token --json '["SET","key","value"]' ``` `--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). The values come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. diff --git a/README.md b/README.md index 0394afe..18e5e98 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,9 @@ upstash redis --help ### Execute via REST (`redis exec` does not use the Developer API key) ```bash -upstash redis exec --db-url --db-token --command "SET key value" -upstash redis exec --db-url --db-token --command "GET key" +upstash redis exec --db-url --db-token SET key value +upstash redis exec --db-url --db-token GET key +upstash redis exec --db-url --db-token --json '["SET","key","value"]' ``` `--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). Use `endpoint` and `rest_token` from `upstash redis get --db-id `. diff --git a/src/commands/redis/exec.ts b/src/commands/redis/exec.ts index 9e8799e..8d03f67 100644 --- a/src/commands/redis/exec.ts +++ b/src/commands/redis/exec.ts @@ -4,17 +4,21 @@ import { printJSON } from "../../output.js"; interface Flags { dbUrl?: string; dbToken?: string; - command: string; + json?: string; } export function registerExec(redis: Command): void { redis .command("exec") - .description("Execute a Redis command against a database via the REST API") + .description( + "Execute a Redis command against a database via the REST API. " + + "Pass args as positional tokens (shell handles quoting) or via --json." + ) .option("--db-url ", "Database REST URL (overrides UPSTASH_REDIS_REST_URL)") .option("--db-token ", "Database REST token (overrides UPSTASH_REDIS_REST_TOKEN)") - .requiredOption("--command ", 'Redis command to run (e.g. "SET key value")') - .action(async (flags: Flags) => { + .option("--json ", 'Redis command args as a JSON array (e.g. \'["SET","key","val"]\')') + .argument("[args...]", "Command tokens (e.g. SET key value)") + .action(async (positional: string[], flags: Flags) => { const dbUrl = flags.dbUrl ?? process.env.UPSTASH_REDIS_REST_URL; const dbToken = flags.dbToken ?? process.env.UPSTASH_REDIS_REST_TOKEN; @@ -24,9 +28,13 @@ export function registerExec(redis: Command): void { ); } - const args = parseCommand(flags.command); + if (positional.length > 0 && flags.json) { + throw new Error("Provide either positional args or --json, not both."); + } + + const args = flags.json ? parseJsonArgs(flags.json) : positional; if (args.length === 0) { - throw new Error("Empty command"); + throw new Error("Empty command. Pass positional tokens or --json."); } const url = dbUrl.replace(/\/$/, ""); @@ -49,30 +57,15 @@ export function registerExec(redis: Command): void { }); } -export function parseCommand(input: string): string[] { - const args: string[] = []; - let current = ""; - let inSingle = false; - let inDouble = false; - - for (let i = 0; i < input.length; i++) { - const ch = input[i]; - if (ch === undefined) break; - - if (ch === "'" && !inDouble) { - inSingle = !inSingle; - } else if (ch === '"' && !inSingle) { - inDouble = !inDouble; - } else if (ch === " " && !inSingle && !inDouble) { - if (current.length > 0) { - args.push(current); - current = ""; - } - } else { - current += ch; - } +function parseJsonArgs(input: string): string[] { + let parsed: unknown; + try { + parsed = JSON.parse(input); + } catch { + throw new Error(`--json must be valid JSON; got: ${input}`); } - - if (current.length > 0) args.push(current); - return args; + if (!Array.isArray(parsed) || !parsed.every((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean")) { + throw new Error("--json must be a JSON array of strings/numbers/booleans."); + } + return parsed.map((x) => String(x)); } diff --git a/tests/integration/redis.test.ts b/tests/integration/redis.test.ts index 1e8e4fc..a9eabf1 100644 --- a/tests/integration/redis.test.ts +++ b/tests/integration/redis.test.ts @@ -173,7 +173,7 @@ describe("redis exec", () => { "redis", "exec", "--db-url", dbUrl, "--db-token", dbRestToken, - "--command", "SET cli-test-key hello", + "SET", "cli-test-key", "hello", ]) as { result: unknown }; expect(setResult.result).toBe("OK"); @@ -182,7 +182,7 @@ describe("redis exec", () => { "redis", "exec", "--db-url", dbUrl, "--db-token", dbRestToken, - "--command", "GET cli-test-key", + "GET", "cli-test-key", ]) as { result: unknown }; expect(getResult.result).toBe("hello"); @@ -191,7 +191,7 @@ describe("redis exec", () => { "redis", "exec", "--db-url", dbUrl, "--db-token", dbRestToken, - "--command", "DEL cli-test-key", + "DEL", "cli-test-key", ]); }); @@ -203,7 +203,7 @@ describe("redis exec", () => { "redis", "exec", "--db-url", `https://${dbEndpoint}`, "--db-token", dbRestToken, - "--command", "PING", + "PING", ]) as { result: unknown }; expect(result.result).toBe("PONG"); }); diff --git a/tests/unit/parseCommand.test.ts b/tests/unit/parseCommand.test.ts deleted file mode 100644 index 7c1ddbe..0000000 --- a/tests/unit/parseCommand.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseCommand } from "../../src/commands/redis/exec.js"; - -describe("parseCommand", () => { - it("splits simple tokens", () => { - expect(parseCommand("SET key value")).toEqual(["SET", "key", "value"]); - }); - - it("handles double-quoted string with spaces", () => { - expect(parseCommand('SET key "hello world"')).toEqual(["SET", "key", "hello world"]); - }); - - it("handles single-quoted string with spaces", () => { - expect(parseCommand("SET key 'hello world'")).toEqual(["SET", "key", "hello world"]); - }); - - it("strips surrounding quotes", () => { - expect(parseCommand('"SET" key')).toEqual(["SET", "key"]); - }); - - it("returns empty array for empty string", () => { - expect(parseCommand("")).toEqual([]); - }); - - it("ignores extra spaces between tokens", () => { - expect(parseCommand("SET key value")).toEqual(["SET", "key", "value"]); - }); - - it("handles single quotes inside double-quoted string", () => { - expect(parseCommand(`SET key "it's fine"`)).toEqual(["SET", "key", "it's fine"]); - }); - - it("handles double quotes inside single-quoted string", () => { - expect(parseCommand(`SET key 'say "hello"'`)).toEqual(["SET", "key", 'say "hello"']); - }); - - it("handles single token with no spaces", () => { - expect(parseCommand("PING")).toEqual(["PING"]); - }); -}); From 2794704ff72485e423f2ca8b774c47d839637271 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:12:51 +0300 Subject: [PATCH 10/18] fix: delete claude.md file --- CLAUDE.md | 353 ------------------------------------------------------ 1 file changed, 353 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9a4ea70..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,353 +0,0 @@ - -# Upstash CLI — Agent Skill - -The same instructions are packaged for VS Code Agent Skills at `.agents/skills/upstash-cli/SKILL.md`. - -The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and always outputs JSON. - -## Installation - -```bash -npm i -g @upstash/cli -``` - -From a clone: `npm install`, `npm run build`, then `node dist/cli.js …` or `npm link`. - -## Authentication - -Three ways to supply credentials (precedence: flags > env vars > `.env` file): - -**Environment variables:** -```bash -export UPSTASH_EMAIL=you@example.com -export UPSTASH_API_KEY=your_api_key -``` - -**`.env` file** — place a `.env` in the working directory and the CLI loads it automatically: -```bash -UPSTASH_EMAIL=you@example.com -UPSTASH_API_KEY=your_api_key -``` - -**Per-command flags:** `--email ` and `--api-key ` override everything else for that invocation. - -**Custom `.env` path:** pass `--env-file ` as a global flag to load credentials from a specific file: -```bash -upstash --env-file ~/secrets/.env redis list -``` - -**Agents:** You can use a **read-only** Developer API key in `UPSTASH_API_KEY`. The Developer API then only returns what that key is allowed to see, and only the actions allowed for that key will succeed; anything else fails at the API. - -## Global Flags - -Every command accepts these flags: - -| Flag | Description | -|------|-------------| -| `--email ` | Upstash email (overrides `UPSTASH_EMAIL`) | -| `--api-key ` | Upstash API key (overrides `UPSTASH_API_KEY`) | - -**Resource IDs** — scoped commands use `--db-id`, `--index-id`, `--qstash-id`, or `--team-id` followed by the placeholder **``** (e.g. `--index-id `), including in `--help` output. - -| Flag | Products | -|------|----------| -| `--db-id ` | Redis | -| `--index-id ` | Vector, Search | -| `--qstash-id ` | QStash | -| `--team-id ` | Team (`delete`, `members`; also `add-member` / `remove-member`) | - -## Output Format - -All commands output JSON to stdout. Errors output `{ "error": "..." }` to stderr and exit with code 1. - -### Success with data -```json -{ "database_id": "...", "database_name": "mydb", "state": "active", ... } -``` - -### Boolean operation success -```json -{ "success": true, "database_id": "..." } -``` - -### Delete success -```json -{ "deleted": true, "database_id": "..." } -``` - -### Error (exits with code 1) -```json -{ "error": "detailed error message" } -``` - ---- - -## Redis Commands - -### Execute Commands - -```bash -upstash redis exec --db-url --db-token SET key value -upstash redis exec --db-url --db-token GET key -upstash redis exec --db-url --db-token HGETALL myhash -upstash redis exec --db-url --db-token --json '["SET","key","value"]' -``` - -`--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). The values come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. - -### Core CRUD - -```bash -upstash redis list -upstash redis get --db-id -upstash redis get --db-id --hide-credentials # Omit password from output -upstash redis create --name --region -upstash redis create --name --region --read-regions -upstash redis delete --db-id --dry-run # Preview before deleting -upstash redis delete --db-id -upstash redis rename --db-id --name -upstash redis reset-password --db-id -upstash redis stats --db-id -``` - -### Available Redis Regions - -**AWS:** `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1` - -**GCP:** `us-central1`, `us-east4`, `europe-west1`, `asia-northeast1` - -### Configuration - -```bash -upstash redis enable-tls --db-id -upstash redis enable-eviction --db-id -upstash redis disable-eviction --db-id -upstash redis enable-autoupgrade --db-id -upstash redis disable-autoupgrade --db-id -upstash redis change-plan --db-id --plan # Plans: free, payg, pro, paid -upstash redis update-budget --db-id --budget # Monthly budget in cents -upstash redis update-regions --db-id --read-regions -upstash redis move-to-team --db-id --team-id -``` - -### Backups - -```bash -upstash redis backup list --db-id -upstash redis backup create --db-id --name -upstash redis backup delete --db-id --backup-id --dry-run -upstash redis backup delete --db-id --backup-id -upstash redis backup restore --db-id --backup-id -upstash redis backup enable-daily --db-id -upstash redis backup disable-daily --db-id -``` - -### Redis Database Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `database_id` | string | Unique identifier (UUID) | -| `database_name` | string | Display name | -| `endpoint` | string | Redis connection hostname | -| `port` | number | Redis port | -| `password` | string | Redis password (omitted with `--hide-credentials`) | -| `state` | string | `active`, `suspended`, or `passive` | -| `tls` | boolean | TLS enabled | -| `type` | string | `free`, `payg`, `pro`, or `paid` | -| `primary_region` | string | Primary region | -| `read_regions` | string[] | Read replica regions | -| `eviction` | boolean | Key eviction enabled | -| `auto_upgrade` | boolean | Auto version upgrade enabled | -| `daily_backup_enabled` | boolean | Daily backups enabled | -| `budget` | number | Monthly spend cap in cents | -| `creation_time` | number | Unix timestamp | - ---- - -## Team Commands - -```bash -upstash team list -upstash team create --name -upstash team create --name --copy-cc # Copy credit card to team -upstash team delete --team-id --dry-run -upstash team delete --team-id -upstash team members --team-id # List team members -upstash team add-member --team-id --member-email --role -upstash team remove-member --team-id --member-email --dry-run -upstash team remove-member --team-id --member-email -``` - -Member roles: `admin`, `dev`, `finance` - -### Team Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `team_id` | string | Unique team identifier | -| `team_name` | string | Team display name | -| `copy_cc` | boolean | Credit card copied to team | - -### TeamMember Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `team_id` | string | Team identifier | -| `member_email` | string | Member email address | -| `member_role` | string | `owner`, `admin`, `dev`, or `finance` | - ---- - -## Vector Commands - -```bash -upstash vector list -upstash vector get --index-id -upstash vector create --name --region --similarity-function --dimension-count -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 -upstash vector delete --index-id --dry-run -upstash vector delete --index-id -upstash vector rename --index-id --name -upstash vector reset-password --index-id -upstash vector set-plan --index-id --plan # Plans: free, payg, fixed -upstash vector transfer --index-id --target-account -upstash vector stats # Aggregate stats across all indexes -upstash vector index-stats --index-id -upstash vector index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -``` - -### Available Vector Regions - -`eu-west-1`, `us-east-1`, `us-central1` - -### Similarity Functions - -`COSINE`, `EUCLIDEAN`, `DOT_PRODUCT` - -### Index Types - -`DENSE`, `SPARSE`, `HYBRID` - -### Embedding Models - -`BGE_SMALL_EN_V1_5`, `BGE_BASE_EN_V1_5`, `BGE_LARGE_EN_V1_5`, `BGE_M3` - -### Sparse Embedding Models - -`BM25`, `BGE_M3` - -### VectorIndex Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | Unique index identifier | -| `name` | string | Index name | -| `region` | string | Deployment region | -| `similarity_function` | string | Distance metric | -| `dimension_count` | number | Dimensions per vector | -| `index_type` | string | `DENSE`, `SPARSE`, or `HYBRID` | -| `embedding_model` | string | Dense embedding model (if set) | -| `sparse_embedding_model` | string | Sparse embedding model (if set) | -| `endpoint` | string | REST endpoint hostname | -| `token` | string | Read-write auth token | -| `read_only_token` | string | Read-only auth token | -| `type` | string | `free`, `payg`, or `fixed` | -| `max_vector_count` | number | Vector capacity | -| `creation_time` | number | Unix timestamp | - ---- - -## Search Commands - -```bash -upstash search list -upstash search get --index-id -upstash search create --name --region --type -upstash search delete --index-id --dry-run -upstash search delete --index-id -upstash search rename --index-id --name -upstash search reset-password --index-id -upstash search transfer --index-id --target-account -upstash search stats # Aggregate stats across all indexes -upstash search index-stats --index-id -upstash search index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -``` - -### Available Search Regions - -`eu-west-1`, `us-central1` - -### Search Plans - -`free`, `payg`, `fixed` - -### SearchIndex Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | Unique index identifier | -| `name` | string | Index name | -| `region` | string | Deployment region | -| `type` | string | `free`, `payg`, or `fixed` | -| `endpoint` | string | REST endpoint hostname | -| `token` | string | Read-write auth token | -| `read_only_token` | string | Read-only auth token | -| `input_enrichment_enabled` | boolean | Input enrichment enabled | -| `creation_time` | number | Unix timestamp | - ---- - -## QStash Commands - -```bash -upstash qstash list # All instances; map `region` → `id` for other commands -upstash qstash get --qstash-id -upstash qstash rotate-token --qstash-id -upstash qstash set-plan --qstash-id --plan # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m -upstash qstash stats --qstash-id -upstash qstash stats --qstash-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -upstash qstash ipv4 # CIDR blocks for firewall allowlisting -upstash qstash move-to-team --qstash-id --target-team-id -upstash qstash update-budget --qstash-id --budget # 0 = no limit -upstash qstash enable-prodpack --qstash-id -upstash qstash disable-prodpack --qstash-id -``` - -### QStashUser Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | QStash instance identifier | -| `customer_id` | string | Owner email or team ID | -| `token` | string | Auth token for QStash API | -| `state` | string | `active` or `passive` | -| `type` | string | `free` or `paid` | -| `reserved_type` | string | Reserved plan: `paid`, `qstash_fixed_1m`, `qstash_fixed_10m`, `qstash_fixed_100m` | -| `region` | string | `eu-central-1` or `us-east-1` | -| `budget` | number | Monthly spend cap in dollars (0 = no limit) | -| `prod_pack_enabled` | boolean | Production pack active | -| `max_requests_per_day` | number | Daily request soft limit | -| `max_requests_per_second` | number | Rate limit | -| `max_topics` | number | Max topics | -| `max_schedules` | number | Max schedules | -| `max_queues` | number | Max queues | -| `timeout` | number | Request timeout in seconds | -| `creation_time` | number | Unix timestamp | - ---- - -## Tips for Agents - -- All output is JSON — pipe through `jq` for field extraction. -- Exit code `0` = success, `1` = error. -- Use `--dry-run` before any `delete` or `remove-member` command to confirm the target. -- Use `--hide-credentials` on `redis get` when the password is not needed. -- Run `upstash qstash list` first to discover which `id` maps to which `region`, then use those IDs for all other qstash commands. -- Field extraction examples: - ```bash - upstash redis list | jq '.[].database_id' - upstash vector list | jq '.[] | {id, name, region}' - upstash qstash list | jq '.[] | {id, region}' - upstash team members --team-id | jq '.[].member_email' - ``` From afcb86891ce36a68e31ab56d5b9c95f2251b8f08 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:13:10 +0300 Subject: [PATCH 11/18] fix: downgrade dotenv to the stable v16 to avoid advertisements --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf221be..cc883e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "commander": "^13.0.0", - "dotenv": "^17.4.2" + "dotenv": "^16.4.5" }, "bin": { "upstash": "dist/cli.js" @@ -987,9 +987,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 48d56e1..2fc614b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "license": "MIT", "dependencies": { "commander": "^13.0.0", - "dotenv": "^17.4.2" + "dotenv": "^16.4.5" }, "devDependencies": { "@types/node": "^20.10.0", From 428ffb3731750570d048fff97cb8e1d160595e26 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:16:36 +0300 Subject: [PATCH 12/18] refactor: promote --email and --api-key to root-level global flags Declare --email and --api-key once on the root program instead of on every leaf command. Each action resolves auth via optsWithGlobals(), so the flags are accepted in any position on the command line: upstash --email X redis list upstash redis --email X list upstash redis list --email X Removes ~150 lines of duplicated .option(...) declarations and the matching email?/apiKey? fields from every flag type. Positional bug where upstash --email X redis list was previously rejected is fixed. --- src/auth.ts | 11 ++++++++--- src/cli.ts | 4 +++- src/commands/qstash/disable-prodpack.ts | 6 ++---- src/commands/qstash/enable-prodpack.ts | 6 ++---- src/commands/qstash/get.ts | 6 ++---- src/commands/qstash/ipv4.ts | 6 ++---- src/commands/qstash/list.ts | 6 ++---- src/commands/qstash/move-to-team.ts | 6 ++---- src/commands/qstash/rotate-token.ts | 6 ++---- src/commands/qstash/set-plan.ts | 6 ++---- src/commands/qstash/stats.ts | 6 ++---- src/commands/qstash/update-budget.ts | 6 ++---- src/commands/redis/backup/create.ts | 6 ++---- src/commands/redis/backup/delete.ts | 6 ++---- src/commands/redis/backup/disable-daily.ts | 6 ++---- src/commands/redis/backup/enable-daily.ts | 6 ++---- src/commands/redis/backup/list.ts | 6 ++---- src/commands/redis/backup/restore.ts | 6 ++---- src/commands/redis/change-plan.ts | 6 ++---- src/commands/redis/create.ts | 6 ++---- src/commands/redis/delete.ts | 6 ++---- src/commands/redis/disable-autoupgrade.ts | 6 ++---- src/commands/redis/disable-eviction.ts | 6 ++---- src/commands/redis/enable-autoupgrade.ts | 6 ++---- src/commands/redis/enable-eviction.ts | 6 ++---- src/commands/redis/enable-tls.ts | 6 ++---- src/commands/redis/get.ts | 6 ++---- src/commands/redis/list.ts | 6 ++---- src/commands/redis/move-to-team.ts | 6 ++---- src/commands/redis/rename.ts | 6 ++---- src/commands/redis/reset-password.ts | 6 ++---- src/commands/redis/stats.ts | 6 ++---- src/commands/redis/update-budget.ts | 6 ++---- src/commands/redis/update-regions.ts | 6 ++---- src/commands/search/create.ts | 6 ++---- src/commands/search/delete.ts | 6 ++---- src/commands/search/get.ts | 6 ++---- src/commands/search/list.ts | 6 ++---- src/commands/search/rename.ts | 6 ++---- src/commands/search/reset-password.ts | 6 ++---- src/commands/search/stats.ts | 12 ++++-------- src/commands/search/transfer.ts | 6 ++---- src/commands/team/add-member.ts | 6 ++---- src/commands/team/create.ts | 6 ++---- src/commands/team/delete.ts | 6 ++---- src/commands/team/list.ts | 6 ++---- src/commands/team/members.ts | 6 ++---- src/commands/team/remove-member.ts | 6 ++---- src/commands/vector/create.ts | 6 ++---- src/commands/vector/delete.ts | 6 ++---- src/commands/vector/get.ts | 6 ++---- src/commands/vector/list.ts | 6 ++---- src/commands/vector/rename.ts | 6 ++---- src/commands/vector/reset-password.ts | 6 ++---- src/commands/vector/set-plan.ts | 6 ++---- src/commands/vector/stats.ts | 12 ++++-------- src/commands/vector/transfer.ts | 6 ++---- 57 files changed, 125 insertions(+), 232 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index cfa450b..140b9b3 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,9 +3,14 @@ export interface Auth { apiKey: string; } -export function resolveAuth(flags: { email?: string; apiKey?: string }): Auth { - const email = flags.email ?? process.env.UPSTASH_EMAIL; - const apiKey = flags.apiKey ?? process.env.UPSTASH_API_KEY; +import type { Command } from "commander"; + +export function resolveAuth(cmdOrFlags: Command | { email?: string; apiKey?: string }): Auth { + const opts = typeof (cmdOrFlags as Command).optsWithGlobals === "function" + ? (cmdOrFlags as Command).optsWithGlobals() + : cmdOrFlags; + const email = (opts as { email?: string }).email ?? process.env.UPSTASH_EMAIL; + const apiKey = (opts as { apiKey?: string }).apiKey ?? process.env.UPSTASH_API_KEY; if (!email || !apiKey) { throw new Error( diff --git a/src/cli.ts b/src/cli.ts index ee71576..a6bd38b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,7 +26,9 @@ program .name("upstash") .description("Agent-friendly CLI for Upstash") .version(version) - .option("--env-file ", "Path to a .env file to load credentials from"); + .option("--env-file ", "Path to a .env file to load credentials from") + .option("--email ", "Upstash email (overrides UPSTASH_EMAIL)") + .option("--api-key ", "Upstash API key (overrides UPSTASH_API_KEY)"); registerRedis(program); registerTeam(program); diff --git a/src/commands/qstash/disable-prodpack.ts b/src/commands/qstash/disable-prodpack.ts index e861b3a..63b5ab7 100644 --- a/src/commands/qstash/disable-prodpack.ts +++ b/src/commands/qstash/disable-prodpack.ts @@ -8,10 +8,8 @@ export function registerQStashDisableProdpack(qstash: Command): void { .command("disable-prodpack") .description("Disable the production pack for a QStash instance") .requiredOption("--qstash-id ", "QStash instance ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/qstash/disable-prodpack/${flags.qstashId}`); printJSON(result); }); diff --git a/src/commands/qstash/enable-prodpack.ts b/src/commands/qstash/enable-prodpack.ts index 19dc0f0..71af04a 100644 --- a/src/commands/qstash/enable-prodpack.ts +++ b/src/commands/qstash/enable-prodpack.ts @@ -8,10 +8,8 @@ export function registerQStashEnableProdpack(qstash: Command): void { .command("enable-prodpack") .description("Enable the production pack for a QStash instance") .requiredOption("--qstash-id ", "QStash instance ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/qstash/enable-prodpack/${flags.qstashId}`); printJSON(result); }); diff --git a/src/commands/qstash/get.ts b/src/commands/qstash/get.ts index cb5a299..bb886d1 100644 --- a/src/commands/qstash/get.ts +++ b/src/commands/qstash/get.ts @@ -9,10 +9,8 @@ export function registerQStashGet(qstash: Command): void { .command("get") .description("Get details of a QStash instance") .requiredOption("--qstash-id ", "QStash instance ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string }, command: Command) => { + const auth = resolveAuth(command); const q = await request(auth, "GET", `/v2/qstash/user/${flags.qstashId}`); printJSON(q); }); diff --git a/src/commands/qstash/ipv4.ts b/src/commands/qstash/ipv4.ts index 327e7e3..4a5d0bd 100644 --- a/src/commands/qstash/ipv4.ts +++ b/src/commands/qstash/ipv4.ts @@ -7,10 +7,8 @@ export function registerQStashIpv4(qstash: Command): void { qstash .command("ipv4") .description("List IPv4 CIDR blocks used by QStash (for firewall allowlisting)") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const addresses = await request(auth, "GET", "/v2/qstash/ipv4"); printJSON(addresses); }); diff --git a/src/commands/qstash/list.ts b/src/commands/qstash/list.ts index 5ffa50a..244b379 100644 --- a/src/commands/qstash/list.ts +++ b/src/commands/qstash/list.ts @@ -8,10 +8,8 @@ export function registerQStashList(qstash: Command): void { qstash .command("list") .description("List all QStash instances (id and region per deployment)") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const users = await request(auth, "GET", "/v2/qstash/users"); printJSON(users); }); diff --git a/src/commands/qstash/move-to-team.ts b/src/commands/qstash/move-to-team.ts index c5249e4..f63646c 100644 --- a/src/commands/qstash/move-to-team.ts +++ b/src/commands/qstash/move-to-team.ts @@ -9,10 +9,8 @@ export function registerQStashMoveToTeam(qstash: Command): void { .description("Move a QStash instance to a team") .requiredOption("--qstash-id ", "QStash instance ID") .requiredOption("--target-team-id ", "Target team ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string; qstashId: string; targetTeamId: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string; targetTeamId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", "/v2/qstash/move-to-team", { qstash_id: flags.qstashId, target_team_id: flags.targetTeamId }); printJSON(result); }); diff --git a/src/commands/qstash/rotate-token.ts b/src/commands/qstash/rotate-token.ts index 1fac7c5..884e3e5 100644 --- a/src/commands/qstash/rotate-token.ts +++ b/src/commands/qstash/rotate-token.ts @@ -9,10 +9,8 @@ export function registerQStashRotateToken(qstash: Command): void { .command("rotate-token") .description("Reset the authentication token for a QStash instance") .requiredOption("--qstash-id ", "QStash instance ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string }, command: Command) => { + const auth = resolveAuth(command); const q = await request(auth, "POST", `/v2/qstash/rotate-token/${flags.qstashId}`); printJSON(q); }); diff --git a/src/commands/qstash/set-plan.ts b/src/commands/qstash/set-plan.ts index 54dbf24..418d387 100644 --- a/src/commands/qstash/set-plan.ts +++ b/src/commands/qstash/set-plan.ts @@ -10,10 +10,8 @@ export function registerQStashSetPlan(qstash: Command): void { .description(`Change the plan for a QStash instance. Plans: ${QSTASH_PLANS.join(", ")}`) .requiredOption("--qstash-id ", "QStash instance ID") .requiredOption("--plan ", `Target plan (${QSTASH_PLANS.join(", ")})`) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string; plan: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string; plan: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/qstash/set-plan/${flags.qstashId}`, { plan_name: flags.plan }); printJSON(result); }); diff --git a/src/commands/qstash/stats.ts b/src/commands/qstash/stats.ts index bfda1ba..8f85012 100644 --- a/src/commands/qstash/stats.ts +++ b/src/commands/qstash/stats.ts @@ -10,10 +10,8 @@ export function registerQStashStats(qstash: Command): void { .description("Get usage statistics for a QStash instance") .requiredOption("--qstash-id ", "QStash instance ID") .option("--period ", `Time period. Available: ${STATS_PERIODS.join(", ")}`, "1h") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string; period?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { qstashId: string; period?: string }, command: Command) => { + const auth = resolveAuth(command); const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; const stats = await request>(auth, "GET", `/v2/qstash/stats/${flags.qstashId}${qs}`); printJSON(stats); diff --git a/src/commands/qstash/update-budget.ts b/src/commands/qstash/update-budget.ts index 8e28df1..82bbdb8 100644 --- a/src/commands/qstash/update-budget.ts +++ b/src/commands/qstash/update-budget.ts @@ -9,13 +9,11 @@ export function registerQStashUpdateBudget(qstash: Command): void { .description("Update the monthly spend budget for a QStash instance (in dollars, 0 = no limit)") .requiredOption("--qstash-id ", "QStash instance ID") .requiredOption("--budget ", "Monthly budget in dollars (0 = no limit)", parseInt) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { qstashId: string; email?: string; apiKey?: string; budget: number }) => { + .action(async (flags: { qstashId: string; budget: number }, command: Command) => { if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { throw new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (dollars).`); } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); const result = await request(auth, "PATCH", `/v2/qstash/update-budget/${flags.qstashId}`, { budget: flags.budget }); printJSON(result); }); diff --git a/src/commands/redis/backup/create.ts b/src/commands/redis/backup/create.ts index 5241986..c60817d 100644 --- a/src/commands/redis/backup/create.ts +++ b/src/commands/redis/backup/create.ts @@ -9,10 +9,8 @@ export function registerBackupCreate(backup: Command): void { .description("Create a backup of a Redis database") .requiredOption("--db-id ", "Database ID") .requiredOption("--name ", "Backup name") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; name: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; name: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/create-backup/${flags.dbId}`, { name: flags.name }); printJSON(result); }); diff --git a/src/commands/redis/backup/delete.ts b/src/commands/redis/backup/delete.ts index 790fdcb..8722ba6 100644 --- a/src/commands/redis/backup/delete.ts +++ b/src/commands/redis/backup/delete.ts @@ -10,14 +10,12 @@ export function registerBackupDelete(backup: Command): void { .requiredOption("--db-id ", "Database ID") .requiredOption("--backup-id ", "Backup ID") .option("--dry-run", "Preview the action without executing it") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; backupId: string; dryRun?: boolean; email?: string; apiKey?: string }) => { + .action(async (flags: { dbId: string; backupId: string; dryRun?: boolean }, command: Command) => { if (flags.dryRun) { printJSON({ action: "delete-backup", database_id: flags.dbId, backup_id: flags.backupId, dry_run: true }); return; } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); await request(auth, "DELETE", `/v2/redis/delete-backup/${flags.dbId}/${flags.backupId}`); printJSON({ deleted: true, backup_id: flags.backupId }); }); diff --git a/src/commands/redis/backup/disable-daily.ts b/src/commands/redis/backup/disable-daily.ts index d5fa75b..dddcbad 100644 --- a/src/commands/redis/backup/disable-daily.ts +++ b/src/commands/redis/backup/disable-daily.ts @@ -8,10 +8,8 @@ export function registerDisableDaily(backup: Command): void { .command("disable-daily") .description("Disable daily automatic backups for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/disable-dailybackup/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/backup/enable-daily.ts b/src/commands/redis/backup/enable-daily.ts index faa833a..429cdc6 100644 --- a/src/commands/redis/backup/enable-daily.ts +++ b/src/commands/redis/backup/enable-daily.ts @@ -8,10 +8,8 @@ export function registerEnableDaily(backup: Command): void { .command("enable-daily") .description("Enable daily automatic backups for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/enable-dailybackup/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/backup/list.ts b/src/commands/redis/backup/list.ts index 733f70e..13781e8 100644 --- a/src/commands/redis/backup/list.ts +++ b/src/commands/redis/backup/list.ts @@ -9,10 +9,8 @@ export function registerBackupList(backup: Command): void { .command("list") .description("List all backups for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const backups = await request(auth, "GET", `/v2/redis/list-backup/${flags.dbId}`); printJSON(backups); }); diff --git a/src/commands/redis/backup/restore.ts b/src/commands/redis/backup/restore.ts index 7b67803..eb7cdb2 100644 --- a/src/commands/redis/backup/restore.ts +++ b/src/commands/redis/backup/restore.ts @@ -9,10 +9,8 @@ export function registerBackupRestore(backup: Command): void { .description("Restore a Redis database from a backup") .requiredOption("--db-id ", "Database ID") .requiredOption("--backup-id ", "ID of the backup to restore from") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; backupId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; backupId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/restore-backup/${flags.dbId}`, { backup_id: flags.backupId }); printJSON(result); }); diff --git a/src/commands/redis/change-plan.ts b/src/commands/redis/change-plan.ts index e8b22c9..db21115 100644 --- a/src/commands/redis/change-plan.ts +++ b/src/commands/redis/change-plan.ts @@ -9,10 +9,8 @@ export function registerChangePlan(redis: Command): void { .description("Change the pricing plan of a Redis database. Plans: free, payg, pro, paid") .requiredOption("--db-id ", "Database ID") .requiredOption("--plan ", "Plan type (free, payg, pro, paid)") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; plan: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; plan: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/change-plan/${flags.dbId}`, { plan: flags.plan }); printJSON(result); }); diff --git a/src/commands/redis/create.ts b/src/commands/redis/create.ts index 077705e..d9a44f0 100644 --- a/src/commands/redis/create.ts +++ b/src/commands/redis/create.ts @@ -12,13 +12,11 @@ export function registerCreate(redis: Command): void { .requiredOption("--name ", "Database name") .requiredOption("--region ", `Primary region. Available: ${REGIONS.join(", ")}`) .option("--read-regions ", "Read replica regions (space-separated)") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; readRegions?: string[] }) => { + .action(async (flags: { name: string; region: string; readRegions?: string[] }, command: Command) => { if (!(REGIONS as readonly string[]).includes(flags.region)) { throw new Error(`Invalid region '${flags.region}'. Available: ${REGIONS.join(", ")}`); } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); const db = await request(auth, "POST", "/v2/redis/database", { database_name: flags.name, region: "global", diff --git a/src/commands/redis/delete.ts b/src/commands/redis/delete.ts index f8bfbb1..40ec96f 100644 --- a/src/commands/redis/delete.ts +++ b/src/commands/redis/delete.ts @@ -9,14 +9,12 @@ export function registerDelete(redis: Command): void { .description("Delete a Redis database") .requiredOption("--db-id ", "Database ID") .option("--dry-run", "Preview the action without executing it") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; dryRun?: boolean; email?: string; apiKey?: string }) => { + .action(async (flags: { dbId: string; dryRun?: boolean }, command: Command) => { if (flags.dryRun) { printJSON({ action: "delete", database_id: flags.dbId, dry_run: true }); return; } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); await request(auth, "DELETE", `/v2/redis/database/${flags.dbId}`); printJSON({ deleted: true, database_id: flags.dbId }); }); diff --git a/src/commands/redis/disable-autoupgrade.ts b/src/commands/redis/disable-autoupgrade.ts index d86685d..85a6971 100644 --- a/src/commands/redis/disable-autoupgrade.ts +++ b/src/commands/redis/disable-autoupgrade.ts @@ -8,10 +8,8 @@ export function registerDisableAutoupgrade(redis: Command): void { .command("disable-autoupgrade") .description("Disable automatic version upgrades for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/disable-autoupgrade/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/disable-eviction.ts b/src/commands/redis/disable-eviction.ts index 213dd1e..ae27dab 100644 --- a/src/commands/redis/disable-eviction.ts +++ b/src/commands/redis/disable-eviction.ts @@ -8,10 +8,8 @@ export function registerDisableEviction(redis: Command): void { .command("disable-eviction") .description("Disable key eviction for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/disable-eviction/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/enable-autoupgrade.ts b/src/commands/redis/enable-autoupgrade.ts index 16554ef..6a2f950 100644 --- a/src/commands/redis/enable-autoupgrade.ts +++ b/src/commands/redis/enable-autoupgrade.ts @@ -8,10 +8,8 @@ export function registerEnableAutoupgrade(redis: Command): void { .command("enable-autoupgrade") .description("Enable automatic version upgrades for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/enable-autoupgrade/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/enable-eviction.ts b/src/commands/redis/enable-eviction.ts index 606c20e..6aa6b4a 100644 --- a/src/commands/redis/enable-eviction.ts +++ b/src/commands/redis/enable-eviction.ts @@ -8,10 +8,8 @@ export function registerEnableEviction(redis: Command): void { .command("enable-eviction") .description("Enable key eviction for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/enable-eviction/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/enable-tls.ts b/src/commands/redis/enable-tls.ts index 6e8e129..baf3d5c 100644 --- a/src/commands/redis/enable-tls.ts +++ b/src/commands/redis/enable-tls.ts @@ -8,10 +8,8 @@ export function registerEnableTls(redis: Command): void { .command("enable-tls") .description("Enable TLS for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/enable-tls/${flags.dbId}`); printJSON(result); }); diff --git a/src/commands/redis/get.ts b/src/commands/redis/get.ts index b1962c9..ae0b386 100644 --- a/src/commands/redis/get.ts +++ b/src/commands/redis/get.ts @@ -10,10 +10,8 @@ export function registerGet(redis: Command): void { .description("Get details of a Redis database") .requiredOption("--db-id ", "Database ID") .option("--hide-credentials", "Omit password from output") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; hideCredentials?: boolean; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; hideCredentials?: boolean }, command: Command) => { + const auth = resolveAuth(command); const qs = flags.hideCredentials ? "?credentials=hide" : ""; const db = await request(auth, "GET", `/v2/redis/database/${flags.dbId}${qs}`); printJSON(db); diff --git a/src/commands/redis/list.ts b/src/commands/redis/list.ts index 823f228..c7615ea 100644 --- a/src/commands/redis/list.ts +++ b/src/commands/redis/list.ts @@ -8,10 +8,8 @@ export function registerList(redis: Command): void { redis .command("list") .description("List all Redis databases") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const dbs = await request(auth, "GET", "/v2/redis/databases"); printJSON(dbs); }); diff --git a/src/commands/redis/move-to-team.ts b/src/commands/redis/move-to-team.ts index 702b56b..5f08cbf 100644 --- a/src/commands/redis/move-to-team.ts +++ b/src/commands/redis/move-to-team.ts @@ -9,10 +9,8 @@ export function registerMoveToTeam(redis: Command): void { .description("Move a Redis database to a team account") .requiredOption("--db-id ", "Database ID") .requiredOption("--team-id ", "Target team ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; teamId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; teamId: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/move-to-team`, { database_id: flags.dbId, team_id: flags.teamId }); printJSON(result); }); diff --git a/src/commands/redis/rename.ts b/src/commands/redis/rename.ts index 9c721b0..ad146af 100644 --- a/src/commands/redis/rename.ts +++ b/src/commands/redis/rename.ts @@ -10,10 +10,8 @@ export function registerRename(redis: Command): void { .description("Rename a Redis database") .requiredOption("--db-id ", "Database ID") .requiredOption("--name ", "New database name") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; name: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; name: string }, command: Command) => { + const auth = resolveAuth(command); const db = await request(auth, "POST", `/v2/redis/rename/${flags.dbId}`, { name: flags.name }); printJSON(db); }); diff --git a/src/commands/redis/reset-password.ts b/src/commands/redis/reset-password.ts index a43aee6..45049d1 100644 --- a/src/commands/redis/reset-password.ts +++ b/src/commands/redis/reset-password.ts @@ -9,10 +9,8 @@ export function registerResetPassword(redis: Command): void { .command("reset-password") .description("Reset the password of a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const db = await request(auth, "POST", `/v2/redis/reset-password/${flags.dbId}`); printJSON(db); }); diff --git a/src/commands/redis/stats.ts b/src/commands/redis/stats.ts index e49316d..d8c6647 100644 --- a/src/commands/redis/stats.ts +++ b/src/commands/redis/stats.ts @@ -8,10 +8,8 @@ export function registerStats(redis: Command): void { .command("stats") .description("Get usage statistics for a Redis database") .requiredOption("--db-id ", "Database ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string }, command: Command) => { + const auth = resolveAuth(command); const stats = await request>(auth, "GET", `/v2/redis/stats/${flags.dbId}`); printJSON(stats); }); diff --git a/src/commands/redis/update-budget.ts b/src/commands/redis/update-budget.ts index ab2f365..2a6a399 100644 --- a/src/commands/redis/update-budget.ts +++ b/src/commands/redis/update-budget.ts @@ -9,13 +9,11 @@ export function registerUpdateBudget(redis: Command): void { .description("Update the monthly spend budget for a Redis database (in cents)") .requiredOption("--db-id ", "Database ID") .requiredOption("--budget ", "Monthly budget in cents", parseInt) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; budget: number; email?: string; apiKey?: string }) => { + .action(async (flags: { dbId: string; budget: number }, command: Command) => { if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { throw new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (cents).`); } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); const result = await request(auth, "PATCH", `/v2/redis/update-budget/${flags.dbId}`, { budget: flags.budget }); printJSON(result); }); diff --git a/src/commands/redis/update-regions.ts b/src/commands/redis/update-regions.ts index 17c13c9..3e4a902 100644 --- a/src/commands/redis/update-regions.ts +++ b/src/commands/redis/update-regions.ts @@ -10,10 +10,8 @@ export function registerUpdateRegions(redis: Command): void { .description("Update read replica regions for a Redis database") .requiredOption("--db-id ", "Database ID") .requiredOption("--read-regions ", `Read replica regions. Available: ${REGIONS.join(", ")}`) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { dbId: string; readRegions: string[]; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { dbId: string; readRegions: string[] }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/redis/update-regions/${flags.dbId}`, { read_regions: flags.readRegions }); printJSON(result); }); diff --git a/src/commands/search/create.ts b/src/commands/search/create.ts index 61370ce..ee3104b 100644 --- a/src/commands/search/create.ts +++ b/src/commands/search/create.ts @@ -12,10 +12,8 @@ export function registerSearchCreate(search: Command): void { .requiredOption("--name ", "Index name") .requiredOption("--region ", `Region. Available: ${SEARCH_REGIONS.join(", ")}`) .requiredOption("--type ", `Plan type. Available: ${SEARCH_PLANS.join(", ")}`) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; type: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { name: string; region: string; type: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "POST", "/v2/search", { name: flags.name, region: flags.region, type: flags.type }); printJSON(idx); }); diff --git a/src/commands/search/delete.ts b/src/commands/search/delete.ts index 8b6fa52..ba2dc6c 100644 --- a/src/commands/search/delete.ts +++ b/src/commands/search/delete.ts @@ -8,15 +8,13 @@ export function registerSearchDelete(search: Command): void { .command("delete") .description("Delete a search index") .requiredOption("--index-id ", "Search index ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") .option("--dry-run", "Preview the action without executing it") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; dryRun?: boolean }) => { + .action(async (flags: { indexId: string; dryRun?: boolean }, command: Command) => { if (flags.dryRun) { printJSON({ action: "delete", index_id: flags.indexId, dry_run: true }); return; } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); await request(auth, "DELETE", `/v2/search/${flags.indexId}`); printJSON({ deleted: true, index_id: flags.indexId }); }); diff --git a/src/commands/search/get.ts b/src/commands/search/get.ts index b44a80a..5da3ccd 100644 --- a/src/commands/search/get.ts +++ b/src/commands/search/get.ts @@ -9,10 +9,8 @@ export function registerSearchGet(search: Command): void { .command("get") .description("Get details of a search index") .requiredOption("--index-id ", "Search index ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "GET", `/v2/search/${flags.indexId}`); printJSON(idx); }); diff --git a/src/commands/search/list.ts b/src/commands/search/list.ts index e7f0300..61f8039 100644 --- a/src/commands/search/list.ts +++ b/src/commands/search/list.ts @@ -8,10 +8,8 @@ export function registerSearchList(search: Command): void { search .command("list") .description("List all search indexes") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const indexes = await request(auth, "GET", "/v2/search"); printJSON(indexes); }); diff --git a/src/commands/search/rename.ts b/src/commands/search/rename.ts index 0c994ab..bcf50ed 100644 --- a/src/commands/search/rename.ts +++ b/src/commands/search/rename.ts @@ -10,10 +10,8 @@ export function registerSearchRename(search: Command): void { .description("Rename a search index") .requiredOption("--index-id ", "Search index ID") .requiredOption("--name ", "New index name") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; name: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; name: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/rename`, { name: flags.name }); printJSON(idx); }); diff --git a/src/commands/search/reset-password.ts b/src/commands/search/reset-password.ts index a5fae70..896dd0f 100644 --- a/src/commands/search/reset-password.ts +++ b/src/commands/search/reset-password.ts @@ -9,10 +9,8 @@ export function registerSearchResetPassword(search: Command): void { .command("reset-password") .description("Reset tokens for a search index") .requiredOption("--index-id ", "Search index ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "POST", `/v2/search/${flags.indexId}/reset-password`); printJSON(idx); }); diff --git a/src/commands/search/stats.ts b/src/commands/search/stats.ts index 448296a..11df4e9 100644 --- a/src/commands/search/stats.ts +++ b/src/commands/search/stats.ts @@ -8,10 +8,8 @@ export function registerSearchStats(search: Command): void { search .command("stats") .description("Get statistics across all search indexes") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const stats = await request>(auth, "GET", "/v2/search/stats"); printJSON(stats); }); @@ -21,10 +19,8 @@ export function registerSearchStats(search: Command): void { .description("Get statistics for a specific search index") .requiredOption("--index-id ", "Search index ID") .option("--period ", `Time period. Available: ${STATS_PERIODS.join(", ")}`, "1h") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; period?: string }, command: Command) => { + const auth = resolveAuth(command); const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; const stats = await request>(auth, "GET", `/v2/search/${flags.indexId}/stats${qs}`); printJSON(stats); diff --git a/src/commands/search/transfer.ts b/src/commands/search/transfer.ts index ccb9e6c..42b29d1 100644 --- a/src/commands/search/transfer.ts +++ b/src/commands/search/transfer.ts @@ -9,10 +9,8 @@ export function registerSearchTransfer(search: Command): void { .description("Transfer a search index to another team") .requiredOption("--index-id ", "Search index ID") .requiredOption("--target-account ", "Target team ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; targetAccount: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; targetAccount: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/search/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); printJSON(result); }); diff --git a/src/commands/team/add-member.ts b/src/commands/team/add-member.ts index e72abf8..b8dc336 100644 --- a/src/commands/team/add-member.ts +++ b/src/commands/team/add-member.ts @@ -12,13 +12,11 @@ export function registerTeamAddMember(team: Command): void { .requiredOption("--team-id ", "Team ID") .requiredOption("--member-email ", "Email of the member to add") .requiredOption("--role ", `Member role (${TEAM_MEMBER_ROLES.join(", ")})`) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string; teamId: string; memberEmail: string; role: string }) => { + .action(async (flags: { teamId: string; memberEmail: string; role: string }, command: Command) => { if (!(TEAM_MEMBER_ROLES as readonly string[]).includes(flags.role)) { throw new Error(`Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`); } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); const member = await request(auth, "POST", "/v2/teams/member", { team_id: flags.teamId, member_email: flags.memberEmail, diff --git a/src/commands/team/create.ts b/src/commands/team/create.ts index 4697eb0..9a1b1f7 100644 --- a/src/commands/team/create.ts +++ b/src/commands/team/create.ts @@ -10,10 +10,8 @@ export function registerTeamCreate(team: Command): void { .description("Create a new team") .requiredOption("--name ", "Team name") .option("--copy-cc", "Copy existing credit card information to the team") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string; name: string; copyCc?: boolean }) => { - const auth = resolveAuth(flags); + .action(async (flags: { name: string; copyCc?: boolean }, command: Command) => { + const auth = resolveAuth(command); const t = await request(auth, "POST", "/v2/team", { team_name: flags.name, copy_cc: flags.copyCc ?? false }); printJSON(t); }); diff --git a/src/commands/team/delete.ts b/src/commands/team/delete.ts index a0f1d29..f1e50fe 100644 --- a/src/commands/team/delete.ts +++ b/src/commands/team/delete.ts @@ -8,15 +8,13 @@ export function registerTeamDelete(team: Command): void { .command("delete") .description("Delete a team") .requiredOption("--team-id ", "Team ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") .option("--dry-run", "Preview the action without executing it") - .action(async (flags: { teamId: string; email?: string; apiKey?: string; dryRun?: boolean }) => { + .action(async (flags: { teamId: string; dryRun?: boolean }, command: Command) => { if (flags.dryRun) { printJSON({ action: "delete", team_id: flags.teamId, dry_run: true }); return; } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); await request(auth, "DELETE", `/v2/team/${flags.teamId}`); printJSON({ deleted: true, team_id: flags.teamId }); }); diff --git a/src/commands/team/list.ts b/src/commands/team/list.ts index b84e090..e45579d 100644 --- a/src/commands/team/list.ts +++ b/src/commands/team/list.ts @@ -8,10 +8,8 @@ export function registerTeamList(team: Command): void { team .command("list") .description("List all teams") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const teams = await request(auth, "GET", "/v2/teams"); printJSON(teams); }); diff --git a/src/commands/team/members.ts b/src/commands/team/members.ts index 1dc8ae4..f017bb3 100644 --- a/src/commands/team/members.ts +++ b/src/commands/team/members.ts @@ -9,10 +9,8 @@ export function registerTeamMembers(team: Command): void { .command("members") .description("List all members of a team") .requiredOption("--team-id ", "Team ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { teamId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { teamId: string }, command: Command) => { + const auth = resolveAuth(command); const members = await request(auth, "GET", `/v2/teams/${flags.teamId}`); printJSON(members); }); diff --git a/src/commands/team/remove-member.ts b/src/commands/team/remove-member.ts index 1e8f449..7930f25 100644 --- a/src/commands/team/remove-member.ts +++ b/src/commands/team/remove-member.ts @@ -9,15 +9,13 @@ export function registerTeamRemoveMember(team: Command): void { .description("Remove a member from a team") .requiredOption("--team-id ", "Team ID") .requiredOption("--member-email ", "Email of the member to remove") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") .option("--dry-run", "Preview the action without executing it") - .action(async (flags: { email?: string; apiKey?: string; dryRun?: boolean; teamId: string; memberEmail: string }) => { + .action(async (flags: { dryRun?: boolean; teamId: string; memberEmail: string }, command: Command) => { if (flags.dryRun) { printJSON({ action: "remove-member", team_id: flags.teamId, member_email: flags.memberEmail, dry_run: true }); return; } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); await request(auth, "DELETE", "/v2/teams/member", { team_id: flags.teamId, member_email: flags.memberEmail }); printJSON({ removed: true, team_id: flags.teamId, member_email: flags.memberEmail }); }); diff --git a/src/commands/vector/create.ts b/src/commands/vector/create.ts index ef54628..3ecb895 100644 --- a/src/commands/vector/create.ts +++ b/src/commands/vector/create.ts @@ -17,13 +17,11 @@ export function registerVectorCreate(vector: Command): void { .option("--embedding-model ", `Embedding model. Available: ${VECTOR_EMBEDDING_MODELS.join(", ")}`) .option("--index-type ", `Index type. Available: ${VECTOR_INDEX_TYPES.join(", ")}`) .option("--sparse-embedding-model ", `Sparse embedding model. Available: ${VECTOR_SPARSE_MODELS.join(", ")}`) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string; name: string; region: string; similarityFunction: string; dimensionCount: number; type?: string; embeddingModel?: string; indexType?: string; sparseEmbeddingModel?: string }) => { + .action(async (flags: { name: string; region: string; similarityFunction: string; dimensionCount: number; type?: string; embeddingModel?: string; indexType?: string; sparseEmbeddingModel?: string }, command: Command) => { if (!Number.isFinite(flags.dimensionCount) || !Number.isInteger(flags.dimensionCount) || flags.dimensionCount < 0) { throw new Error(`Invalid --dimension-count: "${flags.dimensionCount}". Must be a non-negative integer.`); } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); const idx = await request(auth, "POST", "/v2/vector/index", { name: flags.name, region: flags.region, diff --git a/src/commands/vector/delete.ts b/src/commands/vector/delete.ts index 3904f69..c72c444 100644 --- a/src/commands/vector/delete.ts +++ b/src/commands/vector/delete.ts @@ -8,15 +8,13 @@ export function registerVectorDelete(vector: Command): void { .command("delete") .description("Delete a vector index") .requiredOption("--index-id ", "Vector index ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") .option("--dry-run", "Preview the action without executing it") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; dryRun?: boolean }) => { + .action(async (flags: { indexId: string; dryRun?: boolean }, command: Command) => { if (flags.dryRun) { printJSON({ action: "delete", index_id: flags.indexId, dry_run: true }); return; } - const auth = resolveAuth(flags); + const auth = resolveAuth(command); await request(auth, "DELETE", `/v2/vector/index/${flags.indexId}`); printJSON({ deleted: true, index_id: flags.indexId }); }); diff --git a/src/commands/vector/get.ts b/src/commands/vector/get.ts index 1487cc2..ee1860b 100644 --- a/src/commands/vector/get.ts +++ b/src/commands/vector/get.ts @@ -9,10 +9,8 @@ export function registerVectorGet(vector: Command): void { .command("get") .description("Get details of a vector index") .requiredOption("--index-id ", "Vector index ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "GET", `/v2/vector/index/${flags.indexId}`); printJSON(idx); }); diff --git a/src/commands/vector/list.ts b/src/commands/vector/list.ts index 59a3e61..cc15344 100644 --- a/src/commands/vector/list.ts +++ b/src/commands/vector/list.ts @@ -8,10 +8,8 @@ export function registerVectorList(vector: Command): void { vector .command("list") .description("List all vector indexes") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const indexes = await request(auth, "GET", "/v2/vector/index"); printJSON(indexes); }); diff --git a/src/commands/vector/rename.ts b/src/commands/vector/rename.ts index 23adb3d..bbd4a71 100644 --- a/src/commands/vector/rename.ts +++ b/src/commands/vector/rename.ts @@ -10,10 +10,8 @@ export function registerVectorRename(vector: Command): void { .description("Rename a vector index") .requiredOption("--index-id ", "Vector index ID") .requiredOption("--name ", "New index name") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; name: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; name: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/rename`, { name: flags.name }); printJSON(idx); }); diff --git a/src/commands/vector/reset-password.ts b/src/commands/vector/reset-password.ts index a9c1ab0..c7c1b77 100644 --- a/src/commands/vector/reset-password.ts +++ b/src/commands/vector/reset-password.ts @@ -9,10 +9,8 @@ export function registerVectorResetPassword(vector: Command): void { .command("reset-password") .description("Reset tokens for a vector index") .requiredOption("--index-id ", "Vector index ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string }, command: Command) => { + const auth = resolveAuth(command); const idx = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/reset-password`); printJSON(idx); }); diff --git a/src/commands/vector/set-plan.ts b/src/commands/vector/set-plan.ts index 2bcd523..2cba7de 100644 --- a/src/commands/vector/set-plan.ts +++ b/src/commands/vector/set-plan.ts @@ -10,10 +10,8 @@ export function registerVectorSetPlan(vector: Command): void { .description(`Change the plan of a vector index. Plans: ${VECTOR_PLANS.join(", ")}`) .requiredOption("--index-id ", "Vector index ID") .requiredOption("--plan ", `Target plan (${VECTOR_PLANS.join(", ")})`) - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; plan: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; plan: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/setplan`, { target_plan: flags.plan }); printJSON(result); }); diff --git a/src/commands/vector/stats.ts b/src/commands/vector/stats.ts index 6d306af..cd4a963 100644 --- a/src/commands/vector/stats.ts +++ b/src/commands/vector/stats.ts @@ -8,10 +8,8 @@ export function registerVectorStats(vector: Command): void { vector .command("stats") .description("Get statistics across all vector indexes") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { email?: string; apiKey?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: Record, command: Command) => { + const auth = resolveAuth(command); const stats = await request>(auth, "GET", "/v2/vector/index/stats"); printJSON(stats); }); @@ -21,10 +19,8 @@ export function registerVectorStats(vector: Command): void { .description("Get statistics for a specific vector index") .requiredOption("--index-id ", "Vector index ID") .option("--period ", `Time period. Available: ${STATS_PERIODS.join(", ")}`, "1h") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; period?: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; period?: string }, command: Command) => { + const auth = resolveAuth(command); const qs = flags.period ? `?period=${encodeURIComponent(flags.period)}` : ""; const stats = await request>(auth, "GET", `/v2/vector/index/${flags.indexId}/stats${qs}`); printJSON(stats); diff --git a/src/commands/vector/transfer.ts b/src/commands/vector/transfer.ts index cc8b034..0127a92 100644 --- a/src/commands/vector/transfer.ts +++ b/src/commands/vector/transfer.ts @@ -9,10 +9,8 @@ export function registerVectorTransfer(vector: Command): void { .description("Transfer a vector index to another team") .requiredOption("--index-id ", "Vector index ID") .requiredOption("--target-account ", "Target team ID") - .option("--email ", "Upstash email") - .option("--api-key ", "Upstash API key") - .action(async (flags: { indexId: string; email?: string; apiKey?: string; targetAccount: string }) => { - const auth = resolveAuth(flags); + .action(async (flags: { indexId: string; targetAccount: string }, command: Command) => { + const auth = resolveAuth(command); const result = await request(auth, "POST", `/v2/vector/index/${flags.indexId}/transfer`, { target_account: flags.targetAccount }); printJSON(result); }); From a276e4780fff7f1dd84c560fba6006e98e0b1a8f Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:20:47 +0300 Subject: [PATCH 13/18] fix: unwrap API error bodies instead of stringifying them When the Developer API returns a JSON body like {"error":"Unauthorized"}, the CLI was stringifying the whole body and wrapping it again, producing {"error":"{\"error\":\"Unauthorized\"}"}. Parse the body and surface the inner .error or .message string directly so callers see a single unwrapped error field. --- src/client.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client.ts b/src/client.ts index 331e329..0a16de8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -21,6 +21,15 @@ export async function request( const text = await response.text(); if (!response.ok) { + try { + const parsed = JSON.parse(text) as { error?: unknown; message?: unknown }; + const msg = parsed.error ?? parsed.message; + if (typeof msg === "string" && msg.length > 0) { + throw new Error(msg); + } + } catch (err) { + if (err instanceof Error && err.message && !(err instanceof SyntaxError)) throw err; + } throw new Error(text || `HTTP ${response.status}`); } From f55aa91e5bc2d1cc765ecc29c46cd4244c4c3402 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:20:53 +0300 Subject: [PATCH 14/18] refactor: drop redundant enum validation, tighten numeric coercers - redis create --region, team add-member --role: remove client-side enum checks. The API is the source of truth and the local enum lists drift; agents can read the returned error just fine. - redis/qstash update-budget --budget and vector create --dimension-count: replace bare parseInt with an explicit coercer that rejects garbage like "10abc" (parseInt silently truncated it to 10) and negatives, failing fast at parse time. --- src/commands/qstash/update-budget.ts | 17 ++++++++++++----- src/commands/redis/create.ts | 3 --- src/commands/redis/update-budget.ts | 17 ++++++++++++----- src/commands/team/add-member.ts | 3 --- src/commands/vector/create.ts | 17 ++++++++++++----- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/commands/qstash/update-budget.ts b/src/commands/qstash/update-budget.ts index 82bbdb8..a9a1a10 100644 --- a/src/commands/qstash/update-budget.ts +++ b/src/commands/qstash/update-budget.ts @@ -1,18 +1,25 @@ -import { Command } from "commander"; +import { Command, InvalidArgumentError } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON } from "../../output.js"; +function parseNonNegativeInt(name: string) { + return (v: string): number => { + const n = Number(v); + if (!Number.isInteger(n) || n < 0) { + throw new InvalidArgumentError(`--${name} must be a non-negative integer; got "${v}"`); + } + return n; + }; +} + export function registerQStashUpdateBudget(qstash: Command): void { qstash .command("update-budget") .description("Update the monthly spend budget for a QStash instance (in dollars, 0 = no limit)") .requiredOption("--qstash-id ", "QStash instance ID") - .requiredOption("--budget ", "Monthly budget in dollars (0 = no limit)", parseInt) + .requiredOption("--budget ", "Monthly budget in dollars (0 = no limit)", parseNonNegativeInt("budget")) .action(async (flags: { qstashId: string; budget: number }, command: Command) => { - if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { - throw new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (dollars).`); - } const auth = resolveAuth(command); const result = await request(auth, "PATCH", `/v2/qstash/update-budget/${flags.qstashId}`, { budget: flags.budget }); printJSON(result); diff --git a/src/commands/redis/create.ts b/src/commands/redis/create.ts index d9a44f0..d2b4698 100644 --- a/src/commands/redis/create.ts +++ b/src/commands/redis/create.ts @@ -13,9 +13,6 @@ export function registerCreate(redis: Command): void { .requiredOption("--region ", `Primary region. Available: ${REGIONS.join(", ")}`) .option("--read-regions ", "Read replica regions (space-separated)") .action(async (flags: { name: string; region: string; readRegions?: string[] }, command: Command) => { - if (!(REGIONS as readonly string[]).includes(flags.region)) { - throw new Error(`Invalid region '${flags.region}'. Available: ${REGIONS.join(", ")}`); - } const auth = resolveAuth(command); const db = await request(auth, "POST", "/v2/redis/database", { database_name: flags.name, diff --git a/src/commands/redis/update-budget.ts b/src/commands/redis/update-budget.ts index 2a6a399..b10e7a6 100644 --- a/src/commands/redis/update-budget.ts +++ b/src/commands/redis/update-budget.ts @@ -1,18 +1,25 @@ -import { Command } from "commander"; +import { Command, InvalidArgumentError } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON } from "../../output.js"; +function parseNonNegativeInt(name: string) { + return (v: string): number => { + const n = Number(v); + if (!Number.isInteger(n) || n < 0) { + throw new InvalidArgumentError(`--${name} must be a non-negative integer; got "${v}"`); + } + return n; + }; +} + export function registerUpdateBudget(redis: Command): void { redis .command("update-budget") .description("Update the monthly spend budget for a Redis database (in cents)") .requiredOption("--db-id ", "Database ID") - .requiredOption("--budget ", "Monthly budget in cents", parseInt) + .requiredOption("--budget ", "Monthly budget in cents", parseNonNegativeInt("budget")) .action(async (flags: { dbId: string; budget: number }, command: Command) => { - if (!Number.isFinite(flags.budget) || !Number.isInteger(flags.budget) || flags.budget < 0) { - throw new Error(`Invalid --budget: "${flags.budget}". Must be a non-negative integer (cents).`); - } const auth = resolveAuth(command); const result = await request(auth, "PATCH", `/v2/redis/update-budget/${flags.dbId}`, { budget: flags.budget }); printJSON(result); diff --git a/src/commands/team/add-member.ts b/src/commands/team/add-member.ts index b8dc336..ade04c6 100644 --- a/src/commands/team/add-member.ts +++ b/src/commands/team/add-member.ts @@ -13,9 +13,6 @@ export function registerTeamAddMember(team: Command): void { .requiredOption("--member-email ", "Email of the member to add") .requiredOption("--role ", `Member role (${TEAM_MEMBER_ROLES.join(", ")})`) .action(async (flags: { teamId: string; memberEmail: string; role: string }, command: Command) => { - if (!(TEAM_MEMBER_ROLES as readonly string[]).includes(flags.role)) { - throw new Error(`Invalid role '${flags.role}'. Valid roles: ${TEAM_MEMBER_ROLES.join(", ")}`); - } const auth = resolveAuth(command); const member = await request(auth, "POST", "/v2/teams/member", { team_id: flags.teamId, diff --git a/src/commands/vector/create.ts b/src/commands/vector/create.ts index 3ecb895..0c7341b 100644 --- a/src/commands/vector/create.ts +++ b/src/commands/vector/create.ts @@ -1,10 +1,20 @@ -import { Command } from "commander"; +import { Command, InvalidArgumentError } from "commander"; import { resolveAuth } from "../../auth.js"; import { request } from "../../client.js"; import { printJSON } from "../../output.js"; import { VECTOR_REGIONS, VECTOR_SIMILARITY_FUNCTIONS, VECTOR_INDEX_TYPES, VECTOR_EMBEDDING_MODELS, VECTOR_SPARSE_MODELS, VECTOR_PLANS } from "../../types.js"; import type { VectorIndex } from "../../types.js"; +function parseNonNegativeInt(name: string) { + return (v: string): number => { + const n = Number(v); + if (!Number.isInteger(n) || n < 0) { + throw new InvalidArgumentError(`--${name} must be a non-negative integer; got "${v}"`); + } + return n; + }; +} + export function registerVectorCreate(vector: Command): void { vector .command("create") @@ -12,15 +22,12 @@ export function registerVectorCreate(vector: Command): void { .requiredOption("--name ", "Index name") .requiredOption("--region ", `Region. Available: ${VECTOR_REGIONS.join(", ")}`) .requiredOption("--similarity-function ", `Similarity function. Available: ${VECTOR_SIMILARITY_FUNCTIONS.join(", ")}`) - .requiredOption("--dimension-count ", "Number of dimensions per vector", parseInt) + .requiredOption("--dimension-count ", "Number of dimensions per vector", parseNonNegativeInt("dimension-count")) .option("--type ", `Plan type. Available: ${VECTOR_PLANS.join(", ")}`) .option("--embedding-model ", `Embedding model. Available: ${VECTOR_EMBEDDING_MODELS.join(", ")}`) .option("--index-type ", `Index type. Available: ${VECTOR_INDEX_TYPES.join(", ")}`) .option("--sparse-embedding-model ", `Sparse embedding model. Available: ${VECTOR_SPARSE_MODELS.join(", ")}`) .action(async (flags: { name: string; region: string; similarityFunction: string; dimensionCount: number; type?: string; embeddingModel?: string; indexType?: string; sparseEmbeddingModel?: string }, command: Command) => { - if (!Number.isFinite(flags.dimensionCount) || !Number.isInteger(flags.dimensionCount) || flags.dimensionCount < 0) { - throw new Error(`Invalid --dimension-count: "${flags.dimensionCount}". Must be a non-negative integer.`); - } const auth = resolveAuth(command); const idx = await request(auth, "POST", "/v2/vector/index", { name: flags.name, From 54a608939b18594abe57903af3af51ec2d31629b Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:20:58 +0300 Subject: [PATCH 15/18] fix: accept --env-file=path form in addition to --env-file path The pre-Commander argv scan only matched the space-separated form, so --env-file=/path/to/.env silently fell back to the default .env instead of loading the user-specified file. --- src/cli.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a6bd38b..c633495 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,8 +12,13 @@ import dotenv from "dotenv"; // Pre-scan argv for --env-file before Commander parses, so dotenv loads // the right file before any command action reads process.env. -const envFileIndex = process.argv.indexOf("--env-file"); -const envFilePath = envFileIndex !== -1 ? process.argv[envFileIndex + 1] : undefined; +function findEnvFile(argv: string[]): string | undefined { + const eq = argv.find((a) => a.startsWith("--env-file=")); + if (eq) return eq.slice("--env-file=".length); + const i = argv.indexOf("--env-file"); + return i !== -1 ? argv[i + 1] : undefined; +} +const envFilePath = findEnvFile(process.argv); const dotenvResult = dotenv.config(envFilePath ? { path: envFilePath } : undefined); if (envFilePath && dotenvResult.error) { console.error(JSON.stringify({ error: `Could not load env file: ${envFilePath}` })); From ef6a9b93ee27765cb573b01dc009bce301822ee0 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 15 Apr 2026 13:26:56 +0300 Subject: [PATCH 16/18] rename: --env-file to --env-path to avoid Node builtin collision Node 21+ treats --env-file as its own builtin flag and intercepts it from argv before our script runs, producing a raw "node: : not found" message instead of our JSON error shape. Rename our flag so agents get a consistent parseable error regardless of Node version. Default .env-in-cwd loading is unchanged when the flag is omitted. --- .agents/skills/upstash-cli/SKILL.md | 4 ++-- README.md | 4 ++-- src/cli.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.agents/skills/upstash-cli/SKILL.md b/.agents/skills/upstash-cli/SKILL.md index 94771d0..e09bfee 100644 --- a/.agents/skills/upstash-cli/SKILL.md +++ b/.agents/skills/upstash-cli/SKILL.md @@ -31,9 +31,9 @@ UPSTASH_API_KEY=your_api_key **Per-command flags:** `--email ` and `--api-key ` override everything else for that invocation. -**Custom `.env` path:** pass `--env-file ` as a global flag to load credentials from a specific file: +**Custom `.env` path:** pass `--env-path ` as a global flag to load credentials from a specific file: ```bash -upstash --env-file ~/secrets/.env redis list +upstash --env-path ~/secrets/.env redis list ``` **Agents:** Prefer a **read-only** Developer API key in `UPSTASH_API_KEY` when you can. The API only returns what that key may access, and only actions permitted for read-only keys succeed; the rest fail at the API. diff --git a/README.md b/README.md index 18e5e98..ad40b9b 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ UPSTASH_API_KEY=your_api_key **Per-command flags** — `--email` and `--api-key` override everything else for that invocation. -**Custom `.env` path** — use `--env-file ` to load a file from a specific location: +**Custom `.env` path** — use `--env-path ` to load a file from a specific location: ```bash -upstash --env-file ~/secrets/.env redis list +upstash --env-path ~/secrets/.env redis list ``` Precedence: flags > environment variables > `.env` file. diff --git a/src/cli.ts b/src/cli.ts index c633495..2568965 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,15 +10,15 @@ import { registerQStash } from "./commands/qstash/index.js"; import { handleError } from "./output.js"; import dotenv from "dotenv"; -// Pre-scan argv for --env-file before Commander parses, so dotenv loads +// Pre-scan argv for --env-path before Commander parses, so dotenv loads // the right file before any command action reads process.env. -function findEnvFile(argv: string[]): string | undefined { - const eq = argv.find((a) => a.startsWith("--env-file=")); - if (eq) return eq.slice("--env-file=".length); - const i = argv.indexOf("--env-file"); +function findEnvPath(argv: string[]): string | undefined { + const eq = argv.find((a) => a.startsWith("--env-path=")); + if (eq) return eq.slice("--env-path=".length); + const i = argv.indexOf("--env-path"); return i !== -1 ? argv[i + 1] : undefined; } -const envFilePath = findEnvFile(process.argv); +const envFilePath = findEnvPath(process.argv); const dotenvResult = dotenv.config(envFilePath ? { path: envFilePath } : undefined); if (envFilePath && dotenvResult.error) { console.error(JSON.stringify({ error: `Could not load env file: ${envFilePath}` })); @@ -31,7 +31,7 @@ program .name("upstash") .description("Agent-friendly CLI for Upstash") .version(version) - .option("--env-file ", "Path to a .env file to load credentials from") + .option("--env-path ", "Path to a .env file to load credentials from") .option("--email ", "Upstash email (overrides UPSTASH_EMAIL)") .option("--api-key ", "Upstash API key (overrides UPSTASH_API_KEY)"); From a83e0fb3c6f9d535f0bfebd8dcb62bdaa7f8ee75 Mon Sep 17 00:00:00 2001 From: alitariksahin Date: Wed, 15 Apr 2026 14:37:10 +0300 Subject: [PATCH 17/18] chore: remove skills --- .agents/skills/upstash-cli/SKILL.md | 353 ---------------------------- 1 file changed, 353 deletions(-) delete mode 100644 .agents/skills/upstash-cli/SKILL.md diff --git a/.agents/skills/upstash-cli/SKILL.md b/.agents/skills/upstash-cli/SKILL.md deleted file mode 100644 index e09bfee..0000000 --- a/.agents/skills/upstash-cli/SKILL.md +++ /dev/null @@ -1,353 +0,0 @@ ---- -name: upstash-cli -description: Run the Upstash CLI (`upstash`) against the Upstash Developer API for Redis, Vector, Search, QStash, and teams. Use when listing or managing databases, backups, vector/search indexes, QStash instances, team members, stats, or any non-interactive Upstash automation with JSON output and terminal commands. ---- - -The Upstash CLI (`upstash`) manages Upstash services via the Upstash Developer API. Every command is non-interactive and always outputs JSON. - -## Installation - -```bash -npm i -g @upstash/cli -``` - -From a clone (contributors / unreleased fixes): `npm install`, `npm run build`, then `node dist/cli.js …` or `npm link`. - -## Authentication - -Three ways to supply credentials (precedence: flags > env vars > `.env` file): - -**Environment variables** (recommended for agents): -```bash -export UPSTASH_EMAIL=you@example.com -export UPSTASH_API_KEY=your_api_key -``` - -**`.env` file** — place a `.env` in the working directory and the CLI loads it automatically: -```bash -UPSTASH_EMAIL=you@example.com -UPSTASH_API_KEY=your_api_key -``` - -**Per-command flags:** `--email ` and `--api-key ` override everything else for that invocation. - -**Custom `.env` path:** pass `--env-path ` as a global flag to load credentials from a specific file: -```bash -upstash --env-path ~/secrets/.env redis list -``` - -**Agents:** Prefer a **read-only** Developer API key in `UPSTASH_API_KEY` when you can. The API only returns what that key may access, and only actions permitted for read-only keys succeed; the rest fail at the API. - -## Global Flags - -Every command accepts these flags: - -| Flag | Description | -|------|-------------| -| `--email ` | Upstash email (overrides `UPSTASH_EMAIL`) | -| `--api-key ` | Upstash API key (overrides `UPSTASH_API_KEY`) | - -**Resource IDs** — use **`--flag `** (e.g. `--index-id `), same pattern as `--db-id ` for Redis. - -| Flag | Products | -|------|----------| -| `--db-id ` | Redis | -| `--index-id ` | Vector, Search | -| `--qstash-id ` | QStash | -| `--team-id ` | Team (`delete`, `members`; also `add-member` / `remove-member`) | - -## Output Format - -All commands output JSON to stdout. Errors output `{ "error": "..." }` to stderr and exit with code 1. - -### Success with data -```json -{ "database_id": "...", "database_name": "mydb", "state": "active", ... } -``` - -### Boolean operation success -```json -{ "success": true, "database_id": "..." } -``` - -### Delete success -```json -{ "deleted": true, "database_id": "..." } -``` - -### Error (exits with code 1) -```json -{ "error": "detailed error message" } -``` - ---- - -## Redis Commands - -### Execute Commands - -```bash -upstash redis exec --db-url --db-token SET key value -upstash redis exec --db-url --db-token GET key -upstash redis exec --db-url --db-token HGETALL myhash -upstash redis exec --db-url --db-token --json '["SET","key","value"]' -``` - -`--db-url` and `--db-token` can be omitted if `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` are set (env vars or `.env` file). The values come from a prior `upstash redis get --db-id ` call (`endpoint` and `rest_token` fields). Returns `{ "result": ... }` on success or `{ "error": "..." }` on failure. - -### Core CRUD - -```bash -upstash redis list -upstash redis get --db-id -upstash redis get --db-id --hide-credentials # Omit password from output -upstash redis create --name --region -upstash redis create --name --region --read-regions -upstash redis delete --db-id --dry-run # Preview before deleting -upstash redis delete --db-id -upstash redis rename --db-id --name -upstash redis reset-password --db-id -upstash redis stats --db-id -``` - -### Available Redis Regions - -**AWS:** `us-east-1`, `us-east-2`, `us-west-1`, `us-west-2`, `ca-central-1`, `eu-central-1`, `eu-west-1`, `eu-west-2`, `sa-east-1`, `ap-south-1`, `ap-northeast-1`, `ap-southeast-1`, `ap-southeast-2`, `af-south-1` - -**GCP:** `us-central1`, `us-east4`, `europe-west1`, `asia-northeast1` - -### Configuration - -```bash -upstash redis enable-tls --db-id -upstash redis enable-eviction --db-id -upstash redis disable-eviction --db-id -upstash redis enable-autoupgrade --db-id -upstash redis disable-autoupgrade --db-id -upstash redis change-plan --db-id --plan # Plans: free, payg, pro, paid -upstash redis update-budget --db-id --budget # Monthly budget in cents -upstash redis update-regions --db-id --read-regions -upstash redis move-to-team --db-id --team-id -``` - -### Backups - -```bash -upstash redis backup list --db-id -upstash redis backup create --db-id --name -upstash redis backup delete --db-id --backup-id --dry-run -upstash redis backup delete --db-id --backup-id -upstash redis backup restore --db-id --backup-id -upstash redis backup enable-daily --db-id -upstash redis backup disable-daily --db-id -``` - -### Redis Database Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `database_id` | string | Unique identifier (UUID) | -| `database_name` | string | Display name | -| `endpoint` | string | Redis connection hostname | -| `port` | number | Redis port | -| `password` | string | Redis password (omitted with `--hide-credentials`) | -| `state` | string | `active`, `suspended`, or `passive` | -| `tls` | boolean | TLS enabled | -| `type` | string | `free`, `payg`, `pro`, or `paid` | -| `primary_region` | string | Primary region | -| `read_regions` | string[] | Read replica regions | -| `eviction` | boolean | Key eviction enabled | -| `auto_upgrade` | boolean | Auto version upgrade enabled | -| `daily_backup_enabled` | boolean | Daily backups enabled | -| `budget` | number | Monthly spend cap in cents | -| `creation_time` | number | Unix timestamp | - ---- - -## Team Commands - -```bash -upstash team list -upstash team create --name -upstash team create --name --copy-cc # Copy credit card to team -upstash team delete --team-id --dry-run -upstash team delete --team-id -upstash team members --team-id # List team members -upstash team add-member --team-id --member-email --role -upstash team remove-member --team-id --member-email --dry-run -upstash team remove-member --team-id --member-email -``` - -Member roles: `admin`, `dev`, `finance` - -### Team Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `team_id` | string | Unique team identifier | -| `team_name` | string | Team display name | -| `copy_cc` | boolean | Credit card copied to team | - -### TeamMember Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `team_id` | string | Team identifier | -| `member_email` | string | Member email address | -| `member_role` | string | `owner`, `admin`, `dev`, or `finance` | - ---- - -## Vector Commands - -```bash -upstash vector list -upstash vector get --index-id -upstash vector create --name --region --similarity-function --dimension-count -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 1536 --type payg -upstash vector create --name --region us-east-1 --similarity-function COSINE --dimension-count 0 --index-type HYBRID --embedding-model BGE_M3 --sparse-embedding-model BM25 -upstash vector delete --index-id --dry-run -upstash vector delete --index-id -upstash vector rename --index-id --name -upstash vector reset-password --index-id -upstash vector set-plan --index-id --plan # Plans: free, payg, fixed -upstash vector transfer --index-id --target-account -upstash vector stats # Aggregate stats across all indexes -upstash vector index-stats --index-id -upstash vector index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -``` - -### Available Vector Regions - -`eu-west-1`, `us-east-1`, `us-central1` - -### Similarity Functions - -`COSINE`, `EUCLIDEAN`, `DOT_PRODUCT` - -### Index Types - -`DENSE`, `SPARSE`, `HYBRID` - -### Embedding Models - -`BGE_SMALL_EN_V1_5`, `BGE_BASE_EN_V1_5`, `BGE_LARGE_EN_V1_5`, `BGE_M3` - -### Sparse Embedding Models - -`BM25`, `BGE_M3` - -### VectorIndex Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | Unique index identifier | -| `name` | string | Index name | -| `region` | string | Deployment region | -| `similarity_function` | string | Distance metric | -| `dimension_count` | number | Dimensions per vector | -| `index_type` | string | `DENSE`, `SPARSE`, or `HYBRID` | -| `embedding_model` | string | Dense embedding model (if set) | -| `sparse_embedding_model` | string | Sparse embedding model (if set) | -| `endpoint` | string | REST endpoint hostname | -| `token` | string | Read-write auth token | -| `read_only_token` | string | Read-only auth token | -| `type` | string | `free`, `payg`, or `fixed` | -| `max_vector_count` | number | Vector capacity | -| `creation_time` | number | Unix timestamp | - ---- - -## Search Commands - -```bash -upstash search list -upstash search get --index-id -upstash search create --name --region --type -upstash search delete --index-id --dry-run -upstash search delete --index-id -upstash search rename --index-id --name -upstash search reset-password --index-id -upstash search transfer --index-id --target-account -upstash search stats # Aggregate stats across all indexes -upstash search index-stats --index-id -upstash search index-stats --index-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -``` - -### Available Search Regions - -`eu-west-1`, `us-central1` - -### Search Plans - -`free`, `payg`, `fixed` - -### SearchIndex Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | Unique index identifier | -| `name` | string | Index name | -| `region` | string | Deployment region | -| `type` | string | `free`, `payg`, or `fixed` | -| `endpoint` | string | REST endpoint hostname | -| `token` | string | Read-write auth token | -| `read_only_token` | string | Read-only auth token | -| `input_enrichment_enabled` | boolean | Input enrichment enabled | -| `creation_time` | number | Unix timestamp | - ---- - -## QStash Commands - -```bash -upstash qstash list # All instances; map `region` → `id` for other commands -upstash qstash get --qstash-id -upstash qstash rotate-token --qstash-id -upstash qstash set-plan --qstash-id --plan # Plans: paid, qstash_fixed_1m, qstash_fixed_10m, qstash_fixed_100m -upstash qstash stats --qstash-id -upstash qstash stats --qstash-id --period # Periods: 1h, 3h, 12h, 1d, 3d, 7d, 30d -upstash qstash ipv4 # CIDR blocks for firewall allowlisting -upstash qstash move-to-team --qstash-id --target-team-id -upstash qstash update-budget --qstash-id --budget # 0 = no limit -upstash qstash enable-prodpack --qstash-id -upstash qstash disable-prodpack --qstash-id -``` - -### QStashUser Object Fields - -| Field | Type | Description | -|-------|------|-------------| -| `id` | string | QStash instance identifier | -| `customer_id` | string | Owner email or team ID | -| `token` | string | Auth token for QStash API | -| `state` | string | `active` or `passive` | -| `type` | string | `free` or `paid` | -| `reserved_type` | string | Reserved plan: `paid`, `qstash_fixed_1m`, `qstash_fixed_10m`, `qstash_fixed_100m` | -| `region` | string | `eu-central-1` or `us-east-1` | -| `budget` | number | Monthly spend cap in dollars (0 = no limit) | -| `prod_pack_enabled` | boolean | Production pack active | -| `max_requests_per_day` | number | Daily request soft limit | -| `max_requests_per_second` | number | Rate limit | -| `max_topics` | number | Max topics | -| `max_schedules` | number | Max schedules | -| `max_queues` | number | Max queues | -| `timeout` | number | Request timeout in seconds | -| `creation_time` | number | Unix timestamp | - ---- - -## Tips for Agents - -- All output is JSON — pipe through `jq` for field extraction. -- Exit code `0` = success, `1` = error. -- Use `--dry-run` before any `delete` or `remove-member` command to confirm the target. -- Use `--hide-credentials` on `redis get` when the password is not needed. -- Run `upstash qstash list` first to discover which `id` maps to which `region`, then use those IDs for all other qstash commands. -- Field extraction examples: - ```bash - upstash redis list | jq '.[].database_id' - upstash vector list | jq '.[] | {id, name, region}' - upstash qstash list | jq '.[] | {id, region}' - upstash team members --team-id | jq '.[].member_email' - ``` From 5890fb755671d0bdb5a095387f50d7493d9f681b Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Thu, 16 Apr 2026 13:21:59 +0300 Subject: [PATCH 18/18] feat: add `upstash login` / `upstash logout` for persisted credentials Saves credentials to $XDG_CONFIG_HOME/upstash/config.json (0600). Login verifies against the API before writing; auth failures report a clear plain-text error, other failures surface the original reason. Also tightens resolveAuth: any flag/env auth signal locks to the session tier, so a stale UPSTASH_EMAIL no longer silently mixes with a saved key. --- README.md | 15 +++- src/auth.ts | 30 ++++++-- src/cli.ts | 4 ++ src/client.ts | 19 +++-- src/commands/login.ts | 90 +++++++++++++++++++++++ src/commands/logout.ts | 17 +++++ src/config.ts | 55 ++++++++++++++ src/output.ts | 13 +++- tests/unit/auth.test.ts | 154 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 380 insertions(+), 17 deletions(-) create mode 100644 src/commands/login.ts create mode 100644 src/commands/logout.ts create mode 100644 src/config.ts create mode 100644 tests/unit/auth.test.ts diff --git a/README.md b/README.md index ad40b9b..c2f91db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Upstash CLI -Manage Upstash services from the terminal or automation via the [Upstash Developer API](https://docs.upstash.com/redis/howto/developerapi). Commands are non-interactive; successful output on stdout is always JSON (parse with `jq` or similar). Errors go to stderr as `{ "error": "..." }` with exit code 1. +Manage Upstash services from the terminal or automation via the [Upstash Developer API](https://docs.upstash.com/redis/howto/developerapi). Commands are non-interactive; API-returning commands print JSON on stdout (parse with `jq` or similar). Errors go to stderr as `{ "error": "..." }` with exit code 1. The auth helpers `login` / `logout` print a plain status line. ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/upstash/cli) [![Downloads/week](https://img.shields.io/npm/dw/lstr.svg)](https://npmjs.org/package/@upstash/cli) @@ -19,7 +19,14 @@ Prebuilt binaries (Windows, Linux, macOS Intel and Apple Silicon) are on [GitHub ## Authentication -Pick any one of these three methods: +Pick any one of these methods: + +**`upstash login`** — interactive prompt, or non-interactive with flags. Saves credentials to `$XDG_CONFIG_HOME/upstash/config.json` (default `~/.config/upstash/config.json`, mode `0600`): +```bash +upstash login # interactive +upstash login --email you@example.com --api-key # non-interactive +upstash logout # removes the saved file +``` **Environment variables** ```bash @@ -40,7 +47,7 @@ UPSTASH_API_KEY=your_api_key upstash --env-path ~/secrets/.env redis list ``` -Precedence: flags > environment variables > `.env` file. +Precedence: flags > environment variables > `.env` file > saved config file. For agents, a **read-only** Developer API key is often enough: the API only returns what that key allows, and only those operations succeed—mutations fail at the API like in the console. @@ -60,6 +67,8 @@ Scoped commands use explicit resource flags with a shared placeholder: `--db-id ## Top-level commands ```text +upstash login # Save credentials to the user config file +upstash logout # Delete saved credentials upstash redis # Redis databases upstash team # Teams and members upstash vector # Vector indexes diff --git a/src/auth.ts b/src/auth.ts index 140b9b3..f680b88 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -4,19 +4,35 @@ export interface Auth { } import type { Command } from "commander"; +import { readConfig } from "./config.js"; export function resolveAuth(cmdOrFlags: Command | { email?: string; apiKey?: string }): Auth { const opts = typeof (cmdOrFlags as Command).optsWithGlobals === "function" ? (cmdOrFlags as Command).optsWithGlobals() : cmdOrFlags; - const email = (opts as { email?: string }).email ?? process.env.UPSTASH_EMAIL; - const apiKey = (opts as { apiKey?: string }).apiKey ?? process.env.UPSTASH_API_KEY; + const flagEmail = (opts as { email?: string }).email; + const flagKey = (opts as { apiKey?: string }).apiKey; + const envEmail = process.env.UPSTASH_EMAIL; + const envKey = process.env.UPSTASH_API_KEY; - if (!email || !apiKey) { - throw new Error( - "Authentication required. Provide credentials via --email and --api-key flags, set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables, or add them to a .env file in the current directory." - ); + // If any flag/env auth signal is present, resolve from that tier only — + // don't mix a partial session with the saved config, since that silently + // combines credentials from different accounts. + if (flagEmail || flagKey || envEmail || envKey) { + const email = flagEmail ?? envEmail; + const apiKey = flagKey ?? envKey; + if (!email || !apiKey) { + throw new Error( + "Authentication is incomplete: provide both --email and --api-key, or set both UPSTASH_EMAIL and UPSTASH_API_KEY. Or unset them and run `upstash login` to use saved credentials." + ); + } + return { email, apiKey }; } - return { email, apiKey }; + const stored = readConfig(); + if (stored) return stored; + + throw new Error( + "Authentication required. Run `upstash login` to save credentials, or provide --email and --api-key flags, or set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables (also honored from a .env file in the current directory)." + ); } diff --git a/src/cli.ts b/src/cli.ts index 2568965..165afe8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,6 +7,8 @@ import { registerTeam } from "./commands/team/index.js"; import { registerVector } from "./commands/vector/index.js"; import { registerSearch } from "./commands/search/index.js"; import { registerQStash } from "./commands/qstash/index.js"; +import { registerLogin } from "./commands/login.js"; +import { registerLogout } from "./commands/logout.js"; import { handleError } from "./output.js"; import dotenv from "dotenv"; @@ -35,6 +37,8 @@ program .option("--email ", "Upstash email (overrides UPSTASH_EMAIL)") .option("--api-key ", "Upstash API key (overrides UPSTASH_API_KEY)"); +registerLogin(program); +registerLogout(program); registerRedis(program); registerTeam(program); registerVector(program); diff --git a/src/client.ts b/src/client.ts index 0a16de8..f8f1af2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -2,6 +2,14 @@ import type { Auth } from "./auth.js"; const BASE_URL = "https://api.upstash.com"; +export class HttpError extends Error { + readonly status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} + export async function request( auth: Auth, method: string, @@ -21,16 +29,15 @@ export async function request( const text = await response.text(); if (!response.ok) { + let message = text || `HTTP ${response.status}`; try { const parsed = JSON.parse(text) as { error?: unknown; message?: unknown }; const msg = parsed.error ?? parsed.message; - if (typeof msg === "string" && msg.length > 0) { - throw new Error(msg); - } - } catch (err) { - if (err instanceof Error && err.message && !(err instanceof SyntaxError)) throw err; + if (typeof msg === "string" && msg.length > 0) message = msg; + } catch { + // fall through with the raw text } - throw new Error(text || `HTTP ${response.status}`); + throw new HttpError(message, response.status); } if (text === "" || text === '"OK"') return "OK" as T; diff --git a/src/commands/login.ts b/src/commands/login.ts new file mode 100644 index 0000000..a7d458b --- /dev/null +++ b/src/commands/login.ts @@ -0,0 +1,90 @@ +import { Command } from "commander"; +import { createInterface } from "node:readline"; +import { writeConfig } from "../config.js"; +import { HttpError, request } from "../client.js"; +import { plainError } from "../output.js"; + +export function registerLogin(program: Command): void { + program + .command("login") + .description("Save Upstash credentials to the user config file. Uses --email/--api-key if provided, otherwise prompts interactively.") + .action(async (_flags: unknown, command: Command) => { + const globals = command.optsWithGlobals() as { email?: string; apiKey?: string }; + const email = globals.email ?? await promptLine("Upstash email: "); + if (!globals.apiKey) { + process.stderr.write("Create an API key at https://console.upstash.com/account/api\n"); + } + const apiKey = globals.apiKey ?? await promptHidden("Upstash API key: "); + + if (!email) throw plainError("Email is required."); + if (!apiKey) throw plainError("API key is required."); + + try { + await request({ email, apiKey }, "GET", "/v2/redis/databases"); + } catch (err) { + if (err instanceof HttpError && (err.status === 401 || err.status === 403)) { + throw plainError("Authentication failed: the email and API key combination is not valid."); + } + const reason = err instanceof Error ? err.message : String(err); + throw plainError(`Could not verify credentials: ${reason}`); + } + + const path = writeConfig({ email, apiKey }); + console.log(`Credentials verified and saved to ${path}`); + }); +} + +function promptLine(question: string): Promise { + const rl = createInterface({ input: process.stdin, output: process.stderr }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +function promptHidden(question: string): Promise { + return new Promise((resolve, reject) => { + const stdin = process.stdin; + const stderr = process.stderr; + if (!stdin.isTTY || typeof stdin.setRawMode !== "function") { + promptLine(question).then(resolve, reject); + return; + } + stderr.write(question); + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + let value = ""; + const onData = (chunk: string): void => { + for (const ch of chunk) { + if (ch === "\r" || ch === "\n") { + stdin.setRawMode(false); + stdin.pause(); + stdin.removeListener("data", onData); + stderr.write("\n"); + resolve(value); + return; + } + if (ch === "\u0003") { + stdin.setRawMode(false); + stdin.pause(); + stdin.removeListener("data", onData); + stderr.write("\n"); + process.exit(130); + } + if (ch === "\u007f" || ch === "\b") { + if (value.length > 0) { + value = value.slice(0, -1); + stderr.write("\b \b"); + } + continue; + } + value += ch; + stderr.write("*"); + } + }; + stdin.on("data", onData); + }); +} diff --git a/src/commands/logout.ts b/src/commands/logout.ts new file mode 100644 index 0000000..67cd4b6 --- /dev/null +++ b/src/commands/logout.ts @@ -0,0 +1,17 @@ +import { Command } from "commander"; +import { deleteConfig, getConfigPath } from "../config.js"; + +export function registerLogout(program: Command): void { + program + .command("logout") + .description("Delete saved credentials from the user config file.") + .action(() => { + const path = getConfigPath(); + const removed = deleteConfig(); + if (removed) { + console.log(`Removed credentials at ${path}`); + } else { + console.log(`No saved credentials at ${path}`); + } + }); +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..50f4703 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,55 @@ +import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import type { Auth } from "./auth.js"; + +interface StoredConfig { + email?: string; + api_key?: string; +} + +export function getConfigDir(): string { + const override = process.env.UPSTASH_CONFIG_HOME; + if (override) return override; + const xdg = process.env.XDG_CONFIG_HOME; + const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"); + return join(base, "upstash"); +} + +export function getConfigPath(): string { + return join(getConfigDir(), "config.json"); +} + +export function readConfig(): Auth | null { + const path = getConfigPath(); + if (!existsSync(path)) return null; + let raw: string; + try { + raw = readFileSync(path, "utf8"); + } catch { + return null; + } + let parsed: StoredConfig; + try { + parsed = JSON.parse(raw) as StoredConfig; + } catch { + return null; + } + if (!parsed.email || !parsed.api_key) return null; + return { email: parsed.email, apiKey: parsed.api_key }; +} + +export function writeConfig(auth: Auth): string { + const path = getConfigPath(); + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + const body: StoredConfig = { email: auth.email, api_key: auth.apiKey }; + writeFileSync(path, JSON.stringify(body, null, 2) + "\n", { mode: 0o600 }); + return path; +} + +export function deleteConfig(): boolean { + const path = getConfigPath(); + if (!existsSync(path)) return false; + rmSync(path); + return true; +} diff --git a/src/output.ts b/src/output.ts index 17ec41b..9a1c621 100644 --- a/src/output.ts +++ b/src/output.ts @@ -2,8 +2,19 @@ export function printJSON(data: unknown): void { console.log(JSON.stringify(data, null, 2)); } +export function plainError(message: string): Error { + const err = new Error(message); + (err as { plain?: boolean }).plain = true; + return err; +} + export function handleError(err: unknown): never { const message = err instanceof Error ? err.message : String(err); - console.error(JSON.stringify({ error: message })); + const plain = err instanceof Error && (err as { plain?: boolean }).plain === true; + if (plain) { + console.error(message); + } else { + console.error(JSON.stringify({ error: message })); + } process.exit(1); } diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts new file mode 100644 index 0000000..a6d8ccb --- /dev/null +++ b/tests/unit/auth.test.ts @@ -0,0 +1,154 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mkdtempSync, rmSync, statSync, readFileSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Command } from "commander"; +import { readConfig, writeConfig, deleteConfig, getConfigPath } from "../../src/config.js"; +import { resolveAuth } from "../../src/auth.js"; +import { registerLogin } from "../../src/commands/login.js"; +import { registerLogout } from "../../src/commands/logout.js"; + +let dir: string; +const originalEnv = { ...process.env }; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "upstash-cli-test-")); + process.env.UPSTASH_CONFIG_HOME = dir; + delete process.env.UPSTASH_EMAIL; + delete process.env.UPSTASH_API_KEY; +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + process.env = { ...originalEnv }; +}); + +describe("config file round-trip", () => { + it("returns null when the file is missing", () => { + expect(readConfig()).toBeNull(); + }); + + it("writes with 0600 perms and reads back", () => { + const path = writeConfig({ email: "a@b.com", apiKey: "key-1" }); + expect(path).toBe(getConfigPath()); + expect(readConfig()).toEqual({ email: "a@b.com", apiKey: "key-1" }); + if (process.platform !== "win32") { + const mode = statSync(path).mode & 0o777; + expect(mode).toBe(0o600); + } + const raw = JSON.parse(readFileSync(path, "utf8")) as Record; + expect(raw).toEqual({ email: "a@b.com", api_key: "key-1" }); + }); + + it("deleteConfig reports whether it removed a file", () => { + expect(deleteConfig()).toBe(false); + writeConfig({ email: "a@b.com", apiKey: "key-1" }); + expect(deleteConfig()).toBe(true); + expect(existsSync(getConfigPath())).toBe(false); + }); +}); + +describe("resolveAuth precedence", () => { + it("throws when nothing is configured and mentions `upstash login`", () => { + expect(() => resolveAuth({})).toThrow(/upstash login/); + }); + + it("falls back to the saved config file", () => { + writeConfig({ email: "file@b.com", apiKey: "file-key" }); + expect(resolveAuth({})).toEqual({ email: "file@b.com", apiKey: "file-key" }); + }); + + it("env vars beat the saved config file", () => { + writeConfig({ email: "file@b.com", apiKey: "file-key" }); + process.env.UPSTASH_EMAIL = "env@b.com"; + process.env.UPSTASH_API_KEY = "env-key"; + expect(resolveAuth({})).toEqual({ email: "env@b.com", apiKey: "env-key" }); + }); + + it("refuses to mix a partial session tier with the saved config", () => { + writeConfig({ email: "file@b.com", apiKey: "file-key" }); + process.env.UPSTASH_EMAIL = "env@b.com"; + // no UPSTASH_API_KEY — session tier is partial, must not silently borrow from config + expect(() => resolveAuth({})).toThrow(/incomplete/); + }); + + it("flags beat env vars and the saved config file", () => { + writeConfig({ email: "file@b.com", apiKey: "file-key" }); + process.env.UPSTASH_EMAIL = "env@b.com"; + process.env.UPSTASH_API_KEY = "env-key"; + expect(resolveAuth({ email: "flag@b.com", apiKey: "flag-key" })).toEqual({ + email: "flag@b.com", + apiKey: "flag-key", + }); + }); +}); + +describe("login / logout commands (flag form)", () => { + function authProgram(): Command { + return new Command() + .exitOverride() + .option("--email ", "Upstash email (overrides UPSTASH_EMAIL)") + .option("--api-key ", "Upstash API key (overrides UPSTASH_API_KEY)") + .configureOutput({ writeOut: () => {}, writeErr: () => {} }); + } + + async function captureStdout(program: Command, argv: string[]): Promise { + const lines: string[] = []; + const origLog = console.log; + console.log = (...args: unknown[]) => lines.push(args.join(" ")); + try { + await program.parseAsync(["node", "upstash", ...argv]); + } finally { + console.log = origLog; + } + return lines.join("\n"); + } + + it("login --email --api-key verifies, then writes the config file and reports the path", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("[]", { status: 200 }), + ); + try { + const p = authProgram(); + registerLogin(p); + const output = await captureStdout(p, ["login", "--email", "cli@b.com", "--api-key", "cli-key"]); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(output).toBe(`Credentials verified and saved to ${getConfigPath()}`); + expect(readConfig()).toEqual({ email: "cli@b.com", apiKey: "cli-key" }); + } finally { + fetchSpy.mockRestore(); + } + }); + + it("login rejects invalid credentials without writing the config file", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response('{"error":"unauthorized"}', { status: 401 }), + ); + try { + const p = authProgram(); + registerLogin(p); + await expect( + captureStdout(p, ["login", "--email", "bad@b.com", "--api-key", "bad-key"]), + ).rejects.toThrow(/Authentication failed/); + expect(readConfig()).toBeNull(); + } finally { + fetchSpy.mockRestore(); + } + }); + + it("logout removes the saved file and reports the path", async () => { + writeConfig({ email: "a@b.com", apiKey: "key-1" }); + const p = authProgram(); + registerLogout(p); + const output = await captureStdout(p, ["logout"]); + expect(output).toBe(`Removed credentials at ${getConfigPath()}`); + expect(readConfig()).toBeNull(); + }); + + it("logout on a clean machine says so without erroring", async () => { + const p = authProgram(); + registerLogout(p); + const output = await captureStdout(p, ["logout"]); + expect(output).toBe(`No saved credentials at ${getConfigPath()}`); + }); +});