├── .gitignore ├── src ├── version.ts ├── commands │ ├── redis │ │ ├── types.ts │ │ ├── mod.ts │ │ ├── stats.ts │ │ ├── list.ts │ │ ├── delete.ts │ │ ├── get.ts │ │ ├── enable_multizone_replication.ts │ │ ├── reset_password.ts │ │ ├── rename.ts │ │ ├── move_to_team.ts │ │ └── create.ts │ ├── auth │ │ ├── logout.ts │ │ ├── mod.ts │ │ ├── whoami.ts │ │ └── login.ts │ └── team │ │ ├── mod.ts │ │ ├── list.ts │ │ ├── delete.ts │ │ ├── create.ts │ │ ├── list_members.ts │ │ ├── remove_member.ts │ │ └── add_member.ts ├── deps.ts ├── util │ ├── command.ts │ ├── auth.ts │ └── http.ts ├── config.ts └── mod.ts ├── Makefile ├── LICENSE ├── cmd └── build.ts ├── .github └── workflows │ └── release.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dist 3 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // This is set during build 2 | export const VERSION = "development"; 3 | -------------------------------------------------------------------------------- /src/commands/redis/types.ts: -------------------------------------------------------------------------------- 1 | export type Database = { 2 | database_id: string; 3 | database_name: string; 4 | password: string; 5 | endpoint: string; 6 | port: number; 7 | 8 | region: string; 9 | }; 10 | -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | export * as cliffy from "https://deno.land/x/cliffy@v0.24.0/mod.ts"; 2 | export * as path from "https://deno.land/std@0.139.0/path/mod.ts"; 3 | export * as base64 from "https://deno.land/std@0.139.0/encoding/base64.ts"; 4 | -------------------------------------------------------------------------------- /src/util/command.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../deps.ts"; 2 | 3 | type GlobalConfig = { 4 | upstashEmail?: string; 5 | upstashApiKey?: string; 6 | ci?: boolean; 7 | json?: boolean; 8 | config: string; 9 | }; 10 | 11 | export class Command extends cliffy.Command {} 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | fmt: clean 4 | deno fmt 5 | deno lint 6 | 7 | 8 | clean: 9 | rm -rf dist 10 | 11 | build-node: fmt 12 | deno run -A ./cmd/build.ts 13 | 14 | 15 | build-bin: fmt 16 | deno compile \ 17 | --allow-env \ 18 | --allow-read \ 19 | --allow-write \ 20 | --allow-net \ 21 | --output=./bin/upstash\ 22 | ./src/mod.ts 23 | 24 | -------------------------------------------------------------------------------- /src/commands/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../util/command.ts"; 2 | import { deleteConfig } from "../../config.ts"; 3 | export const logoutCmd = new Command() 4 | .name("logout") 5 | .description("Delete local configuration") 6 | .action((options): void => { 7 | try { 8 | deleteConfig(options.config); 9 | console.log("You have been logged out"); 10 | } catch { 11 | console.log("You were not logged in"); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/commands/auth/mod.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../util/command.ts"; 2 | import { loginCmd } from "./login.ts"; 3 | import { logoutCmd } from "./logout.ts"; 4 | import { whoamiCmd } from "./whoami.ts"; 5 | const authCmd = new Command(); 6 | 7 | authCmd 8 | .description("Login and logout") 9 | .command("login", loginCmd) 10 | .command("logout", logoutCmd) 11 | .command("whoami", whoamiCmd); 12 | 13 | authCmd.reset().action(() => { 14 | authCmd.showHelp(); 15 | }); 16 | 17 | export { authCmd }; 18 | -------------------------------------------------------------------------------- /src/commands/auth/whoami.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { loadConfig } from "../../config.ts"; 4 | export const whoamiCmd = new Command() 5 | .name("whoami") 6 | .description("Return the current users email") 7 | .action((options): void => { 8 | const config = loadConfig(options.config); 9 | if (!config) { 10 | throw new Error("You are not logged in, please run `upstash auth login`"); 11 | } 12 | 13 | console.log( 14 | `Currently logged in as ${cliffy.colors.brightGreen(config.email)}`, 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { path } from "./deps.ts"; 2 | export type Config = { 3 | email: string; 4 | apiKey: string; 5 | }; 6 | const homeDir = Deno.env.get("HOME"); 7 | const fileName = ".upstash.json"; 8 | export const DEFAULT_CONFIG_PATH = homeDir 9 | ? path.join(homeDir, fileName) 10 | : fileName; 11 | 12 | export function loadConfig(path: string): Config | null { 13 | try { 14 | return JSON.parse(Deno.readTextFileSync(path)) as Config; 15 | } catch { 16 | return null; 17 | } 18 | } 19 | 20 | export function storeConfig(path: string, config: Config): void { 21 | Deno.writeTextFileSync(path, JSON.stringify(config)); 22 | } 23 | 24 | export function deleteConfig(path: string): void { 25 | Deno.removeSync(path); 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/team/mod.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../util/command.ts"; 2 | import { createCmd } from "./create.ts"; 3 | import { listCmd } from "./list.ts"; 4 | import { deleteCmd } from "./delete.ts"; 5 | import { addMemberCmd } from "./add_member.ts"; 6 | import { removeMemberCmd } from "./remove_member.ts"; 7 | import { listMembersCmd } from "./list_members.ts"; 8 | 9 | export const teamCmd = new Command() 10 | .description("Manage your teams and their members") 11 | .globalOption("--json=[boolean:boolean]", "Return raw json response") 12 | .command("create", createCmd) 13 | .command("list", listCmd) 14 | .command("delete", deleteCmd) 15 | .command("add-member", addMemberCmd) 16 | .command("remove-member", removeMemberCmd) 17 | .command("list-members", listMembersCmd); 18 | 19 | teamCmd.reset().action(() => { 20 | teamCmd.showHelp(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/util/auth.ts: -------------------------------------------------------------------------------- 1 | import { base64 } from "../deps.ts"; 2 | import { loadConfig } from "../config.ts"; 3 | /** 4 | * Parse cli config and return a basic auth header string 5 | */ 6 | export async function parseAuth(options: { 7 | upstashEmail?: string; 8 | upstashApiKey?: string; 9 | config: string; 10 | ci?: boolean; 11 | [key: string]: unknown; 12 | }): Promise { 13 | let email = options.upstashEmail; 14 | let apiKey = options.upstashApiKey; 15 | const config = loadConfig(options.config); 16 | if (config?.email) { 17 | email = config.email; 18 | } 19 | if (config?.apiKey) { 20 | apiKey = config.apiKey; 21 | } 22 | 23 | if (!email || !apiKey) { 24 | throw new Error( 25 | `Not authenticated, please run "upstash auth login" or specify your config file with "--config=/path/to/.upstash.json"`, 26 | ); 27 | } 28 | 29 | return await Promise.resolve( 30 | `Basic ${base64.encode([email, apiKey].join(":"))}`, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Upstash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/commands/redis/mod.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../util/command.ts"; 2 | import { createCmd } from "./create.ts"; 3 | import { listCmd } from "./list.ts"; 4 | import { deleteCmd } from "./delete.ts"; 5 | import { getCmd } from "./get.ts"; 6 | import { statsCmd } from "./stats.ts"; 7 | import { resetPasswordCmd } from "./reset_password.ts"; 8 | import { renameCmd } from "./rename.ts"; 9 | import { enableMultizoneReplicationCmd } from "./enable_multizone_replication.ts"; 10 | import { moveToTeamCmd } from "./move_to_team.ts"; 11 | const redisCmd = new Command() 12 | .description("Manage redis database instances") 13 | .globalOption("--json=[boolean:boolean]", "Return raw json response") 14 | .command("create", createCmd) 15 | .command("list", listCmd) 16 | .command("get", getCmd) 17 | .command("delete", deleteCmd) 18 | .command("stats", statsCmd) 19 | .command("rename", renameCmd) 20 | .command("reset-password", resetPasswordCmd) 21 | .command("enable-multizone-replication", enableMultizoneReplicationCmd) 22 | .command("move-to-team", moveToTeamCmd); 23 | 24 | redisCmd.reset().action(() => { 25 | redisCmd.showHelp(); 26 | }); 27 | 28 | export { redisCmd }; 29 | -------------------------------------------------------------------------------- /src/commands/team/list.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | export const listCmd = new Command() 7 | .name("list") 8 | .description("list all your teams") 9 | .example("List", "upstash team list") 10 | .action(async (options): Promise => { 11 | const authorization = await parseAuth(options); 12 | 13 | const teams = await http.request<{ team_name: string }[]>({ 14 | method: "GET", 15 | authorization, 16 | path: ["v2", "teams"], 17 | }); 18 | if (options.json) { 19 | console.log(JSON.stringify(teams, null, 2)); 20 | return; 21 | } 22 | 23 | teams.forEach((team) => { 24 | console.log(); 25 | console.log(); 26 | console.log( 27 | cliffy.colors.underline(cliffy.colors.brightGreen(team.team_name)), 28 | ); 29 | console.log(); 30 | console.log( 31 | cliffy.Table.from( 32 | Object.entries(team).map(([k, v]) => [k.toString(), v.toString()]), 33 | ).toString(), 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/util/http.ts: -------------------------------------------------------------------------------- 1 | export type UpstashRequest = { 2 | authorization: string; 3 | method: "GET" | "POST" | "PUT" | "DELETE"; 4 | path?: string[]; 5 | /** 6 | * Request body will be serialized to json 7 | */ 8 | body?: unknown; 9 | }; 10 | 11 | type HttpClientConfig = { 12 | baseUrl: string; 13 | }; 14 | 15 | class HttpClient { 16 | private readonly baseUrl: string; 17 | 18 | public constructor(config: HttpClientConfig) { 19 | this.baseUrl = config.baseUrl.replace(/\/$/, ""); 20 | } 21 | 22 | public async request(req: UpstashRequest): Promise { 23 | if (!req.path) { 24 | req.path = []; 25 | } 26 | 27 | const url = [this.baseUrl, ...req.path].join("/"); 28 | const init: RequestInit = { 29 | method: req.method, 30 | headers: { 31 | "Content-Type": "application/json", 32 | Authorization: req.authorization, 33 | }, 34 | }; 35 | if (req.method !== "GET") { 36 | init.body = JSON.stringify(req.body); 37 | } 38 | 39 | // fetch is defined by isomorphic fetch 40 | // eslint-disable-next-line no-undef 41 | const res = await fetch(url, init); 42 | if (!res.ok) { 43 | throw new Error(await res.text()); 44 | } 45 | return (await res.json()) as TResponse; 46 | } 47 | } 48 | 49 | export const http = new HttpClient({ baseUrl: "https://api.upstash.com" }); 50 | -------------------------------------------------------------------------------- /src/commands/redis/stats.ts: -------------------------------------------------------------------------------- 1 | // import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | import type { Database } from "./types.ts"; 6 | export const statsCmd = new Command() 7 | .name("stats") 8 | .description("Returns detailed information about the databse usage") 9 | .option("--id=", "The id of your database", { required: true }) 10 | .example( 11 | "Get", 12 | `upstash redis stats --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`, 13 | ) 14 | .action(async (options): Promise => { 15 | const authorization = await parseAuth(options); 16 | 17 | // if (!options.id) { 18 | // const dbs = await http.request< 19 | // { database_name: string; database_id: string }[] 20 | // >({ 21 | // method: "GET", 22 | // authorization, 23 | // path: ["v2", "redis", "databases"], 24 | // }); 25 | // options.id = await cliffy.Select.prompt({ 26 | // message: "Select a database to delete", 27 | // options: dbs.map(({ database_name, database_id }) => ({ 28 | // name: database_name, 29 | // value: database_id, 30 | // })), 31 | // }); 32 | // } 33 | 34 | const db = await http.request({ 35 | method: "GET", 36 | authorization, 37 | path: ["v2", "redis", "stats", options.id!], 38 | }); 39 | console.log(JSON.stringify(db, null, 2)); 40 | return; 41 | }); 42 | -------------------------------------------------------------------------------- /src/commands/redis/list.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | import type { Database } from "./types.ts"; 6 | export const listCmd = new Command() 7 | .name("list") 8 | .description("list all your databases") 9 | .option("-e, --expanded ", "Show expanded information") 10 | .example("List", "upstash redis list") 11 | .action(async (options): Promise => { 12 | const authorization = await parseAuth(options); 13 | const dbs = await http.request({ 14 | method: "GET", 15 | authorization, 16 | path: ["v2", "redis", "databases"], 17 | }); 18 | if (options.json) { 19 | console.log(JSON.stringify(dbs, null, 2)); 20 | return; 21 | } 22 | // if (!options.expanded) { 23 | // console.log( 24 | // cliffy.Table.from( 25 | // dbs.map((db) => [db.database_name, db.database_id]), 26 | // ).toString(), 27 | // ); 28 | // return; 29 | // } 30 | 31 | dbs.forEach((db) => { 32 | console.log(); 33 | console.log(); 34 | console.log( 35 | cliffy.colors.underline(cliffy.colors.brightGreen(db.database_name)), 36 | ); 37 | console.log(); 38 | console.log( 39 | cliffy.Table.from( 40 | Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), 41 | ).toString(), 42 | ); 43 | console.log(); 44 | }); 45 | console.log(); 46 | console.log(); 47 | }); 48 | -------------------------------------------------------------------------------- /src/commands/team/delete.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | export const deleteCmd = new Command() 6 | .name("delete") 7 | .description("delete a team") 8 | .option("--id ", "The uuid of your database") 9 | .example("Delete", `upstash team delete f860e7e2-27b8-4166-90d5-ea41e90b4809`) 10 | .action(async (options): Promise => { 11 | const authorization = await parseAuth(options); 12 | 13 | // if (!options.id) { 14 | // if (options.ci) { 15 | // throw new cliffy.ValidationError("id"); 16 | // } 17 | // const teams = await http.request< 18 | // { team_name: string; team_id: string }[] 19 | // >({ 20 | // method: "GET", 21 | // authorization, 22 | // path: ["v2", "teams"], 23 | // }); 24 | // options.id = await cliffy.Select.prompt({ 25 | // message: "Select a team to delete", 26 | // options: teams.map(({ team_name, team_id }) => ({ 27 | // name: team_name, 28 | // value: team_id, 29 | // })), 30 | // }); 31 | // } 32 | 33 | await http.request({ 34 | method: "DELETE", 35 | authorization, 36 | path: ["v2", "team", options.id!], 37 | }); 38 | if (options.json) { 39 | console.log(JSON.stringify({ ok: true }, null, 2)); 40 | return; 41 | } 42 | console.log(cliffy.colors.brightGreen("Team has been deleted")); 43 | console.log(); 44 | }); 45 | -------------------------------------------------------------------------------- /src/commands/team/create.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | export const createCmd = new Command() 7 | .name("create") 8 | .description("Create a new team") 9 | .option("-n --name=", "Name of the database", { 10 | required: true, 11 | }) 12 | .option( 13 | "--copy-credit-card=", 14 | "Set true to copy the credit card information to the new team", 15 | { default: false }, 16 | ) 17 | .action(async (options): Promise => { 18 | const authorization = await parseAuth(options); 19 | 20 | // if (!options.name) { 21 | // if (options.ci) { 22 | // throw new cliffy.ValidationError("name"); 23 | // } 24 | // options.name = await cliffy.Input.prompt("Set a name for your team"); 25 | // } 26 | 27 | const body: Record = { 28 | team_name: options.name, 29 | copy_cc: options.copyCreditCard, 30 | }; 31 | 32 | const team = await http.request({ 33 | method: "POST", 34 | authorization, 35 | path: ["v2", "team"], 36 | body, 37 | }); 38 | if (options.json) { 39 | console.log(JSON.stringify(team, null, 2)); 40 | return; 41 | } 42 | console.log(cliffy.colors.brightGreen("Team has been created")); 43 | console.log(); 44 | console.log( 45 | cliffy.Table.from( 46 | Object.entries(team).map(([k, v]) => [k.toString(), v.toString()]), 47 | ).toString(), 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /src/commands/redis/delete.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | // import type { Database } from "./types.ts"; 6 | export const deleteCmd = new Command() 7 | .name("delete") 8 | .description("delete a redis database") 9 | .option("--id=", "The uuid of the cluster", { required: true }) 10 | .example( 11 | "Delete", 12 | `upstash redis delete --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`, 13 | ) 14 | .action(async (options): Promise => { 15 | const authorization = await parseAuth(options); 16 | 17 | // if (!options.id) { 18 | // if (options.ci) { 19 | // throw new cliffy.ValidationError("id"); 20 | // } 21 | // const dbs = await http.request({ 22 | // method: "GET", 23 | // authorization, 24 | // path: ["v2", "redis", "databases"], 25 | // }); 26 | // options.id = await cliffy.Select.prompt({ 27 | // message: "Select a database to delete", 28 | // options: dbs.map(({ database_name, database_id }) => ({ 29 | // name: database_name, 30 | // value: database_id, 31 | // })), 32 | // }); 33 | // } 34 | 35 | await http.request({ 36 | method: "DELETE", 37 | authorization, 38 | path: ["v2", "redis", "database", options.id!], 39 | }); 40 | if (options.json) { 41 | console.log(JSON.stringify({ ok: true }, null, 2)); 42 | return; 43 | } 44 | console.log(cliffy.colors.brightGreen("Database has been deleted")); 45 | console.log(); 46 | }); 47 | -------------------------------------------------------------------------------- /src/commands/redis/get.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | import type { Database } from "./types.ts"; 6 | 7 | export const getCmd = new Command() 8 | .name("get") 9 | .description("get a redis database") 10 | .option("--id=", "The id of your database", { required: true }) 11 | .example("Get", `upstash redis get --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`) 12 | .action(async (options): Promise => { 13 | const authorization = await parseAuth(options); 14 | 15 | // if (!options.id) { 16 | // if (options.ci) { 17 | // throw new cliffy.ValidationError("id"); 18 | // } 19 | // const dbs = await http.request< 20 | // { database_name: string; database_id: string }[] 21 | // >({ 22 | // method: "GET", 23 | // authorization, 24 | // path: ["v2", "redis", "databases"], 25 | // }); 26 | // options.id = await cliffy.Select.prompt({ 27 | // message: "Select a database to delete", 28 | // options: dbs.map(({ database_name, database_id }) => ({ 29 | // name: database_name, 30 | // value: database_id, 31 | // })), 32 | // }); 33 | // } 34 | 35 | const db = await http.request({ 36 | method: "GET", 37 | authorization, 38 | path: ["v2", "redis", "database", options.id!], 39 | }); 40 | if (options.json) { 41 | console.log(JSON.stringify(db, null, 2)); 42 | return; 43 | } 44 | 45 | console.log( 46 | cliffy.Table.from( 47 | Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), 48 | ).toString(), 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | import { authCmd } from "./commands/auth/mod.ts"; 2 | import { redisCmd } from "./commands/redis/mod.ts"; 3 | import { teamCmd } from "./commands/team/mod.ts"; 4 | import { Command } from "./util/command.ts"; 5 | import { cliffy } from "./deps.ts"; 6 | import { VERSION } from "./version.ts"; 7 | import { DEFAULT_CONFIG_PATH } from "./config.ts"; 8 | const cmd = new Command() 9 | .name("upstash") 10 | .version(VERSION) 11 | .description("Official cli for Upstash products") 12 | .globalEnv("UPSTASH_EMAIL=", "The email you use on upstash") 13 | .globalEnv("UPSTASH_API_KEY=", "The api key from upstash") 14 | // .globalEnv( 15 | // "CI=", 16 | // "Disable interactive prompts and throws an error instead", 17 | // { hidden: true } 18 | // ) 19 | // .globalOption( 20 | // "--non-interactive [boolean]", 21 | // "Disable interactive prompts and throws an error instead", 22 | // { hidden: true } 23 | // ) 24 | .globalOption("-c, --config=", "Path to .upstash.json file", { 25 | default: DEFAULT_CONFIG_PATH, 26 | }) 27 | /** 28 | * Nested commands don't seem to work as expected, or maybe I'm just not understanding them. 29 | * The workaround is to cast as `Command` 30 | */ 31 | .command("auth", authCmd as unknown as Command) 32 | .command("redis", redisCmd as unknown as Command) 33 | .command("team", teamCmd as unknown as Command); 34 | cmd.reset().action(() => { 35 | cmd.showHelp(); 36 | }); 37 | 38 | await cmd.parse(Deno.args).catch((err) => { 39 | if (err instanceof cliffy.ValidationError) { 40 | cmd.showHelp(); 41 | console.error("Usage error: %s", err.message); 42 | Deno.exit(err.exitCode); 43 | } else { 44 | console.error(`Error: ${err.message}`); 45 | 46 | Deno.exit(1); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /cmd/build.ts: -------------------------------------------------------------------------------- 1 | import { build, emptyDir } from "https://deno.land/x/dnt@0.23.0/mod.ts"; 2 | 3 | const packageManager = "npm"; 4 | const outDir = "./dist"; 5 | const version = Deno.args[0] ?? "development"; 6 | 7 | await emptyDir(outDir); 8 | 9 | Deno.writeTextFileSync( 10 | "./src/version.ts", 11 | `export const VERSION = "${version}"`, 12 | ); 13 | 14 | await build({ 15 | packageManager, 16 | entryPoints: [{ kind: "bin", name: "upstash", path: "src/mod.ts" }], 17 | outDir, 18 | shims: { 19 | deno: true, 20 | // undici: true, 21 | custom: [ 22 | { 23 | package: { name: "node-fetch", version: "latest" }, 24 | globalNames: [{ name: "fetch", exportName: "default" }], 25 | }, 26 | ], 27 | }, 28 | 29 | scriptModule: false, 30 | declaration: false, 31 | typeCheck: false, 32 | test: typeof Deno.env.get("TEST") !== "undefined", 33 | package: { 34 | // package.json properties 35 | name: "@upstash/cli", 36 | version, 37 | description: "CLI for Upstash resources.", 38 | repository: { 39 | type: "git", 40 | url: "git+https://github.com/upstash/cli.git", 41 | }, 42 | keywords: ["cli", "redis", "kafka", "serverless", "edge", "upstash"], 43 | contributors: [ 44 | { 45 | name: "Andreas Thomas", 46 | email: "dev@chronark.com", 47 | }, 48 | ], 49 | license: "MIT", 50 | bugs: { 51 | url: "https://github.com/upstash/cli/issues", 52 | }, 53 | homepage: "https://github.com/upstash/cli#readme", 54 | }, 55 | }); 56 | Deno.writeTextFileSync( 57 | "./src/version.ts", 58 | `// This is set during build 59 | export const VERSION = "development";`, 60 | ); 61 | 62 | // post build steps 63 | Deno.copyFileSync("LICENSE", `${outDir}/LICENSE`); 64 | Deno.copyFileSync("README.md", `${outDir}/README.md`); 65 | -------------------------------------------------------------------------------- /src/commands/redis/enable_multizone_replication.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | export const enableMultizoneReplicationCmd = new Command() 7 | .name("enable-multizone-replication") 8 | .description("Enable multizone replication for a redis database") 9 | .option("--id=", "The id of your database", { required: true }) 10 | .example( 11 | "Enable", 12 | `upstash redis enable-multizone-replication f860e7e2-27b8-4166-90d5-ea41e90b4809`, 13 | ) 14 | .action(async (options): Promise => { 15 | const authorization = await parseAuth(options); 16 | 17 | // if (!options.id) { 18 | // if (options.ci) { 19 | // throw new cliffy.ValidationError("id"); 20 | // } 21 | // const dbs = await http.request< 22 | // { database_name: string; database_id: string }[] 23 | // >({ 24 | // method: "GET", 25 | // authorization, 26 | // path: ["v2", "redis", "databases"], 27 | // }); 28 | // options.id = await cliffy.Select.prompt({ 29 | // message: "Select a database to rename", 30 | // options: dbs.map(({ database_name, database_id }) => ({ 31 | // name: database_name, 32 | // value: database_id, 33 | // })), 34 | // }); 35 | // } 36 | 37 | const db = await http.request({ 38 | method: "POST", 39 | authorization, 40 | path: ["v2", "redis", "enable-multizone", options.id!], 41 | }); 42 | if (options.json) { 43 | console.log(JSON.stringify(db, null, 2)); 44 | return; 45 | } 46 | console.log( 47 | cliffy.colors.brightGreen("Multizone replication has been enabled"), 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /src/commands/team/list_members.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | export const listMembersCmd = new Command() 7 | .name("list-members") 8 | .description("List all members of a team") 9 | .option("--id ", "The team id") 10 | .example("List", "upstash team list-members") 11 | .action(async (options): Promise => { 12 | const authorization = await parseAuth(options); 13 | 14 | // if (!options.id) { 15 | // if (options.ci) { 16 | // throw new cliffy.ValidationError("teamID"); 17 | // } 18 | // const teams = await http.request< 19 | // { team_name: string; team_id: string }[] 20 | // >({ 21 | // method: "GET", 22 | // authorization, 23 | // path: ["v2", "teams"], 24 | // }); 25 | // options.id = await cliffy.Select.prompt({ 26 | // message: "Select a team to delete", 27 | // options: teams.map(({ team_name, team_id }) => ({ 28 | // name: team_name, 29 | // value: team_id, 30 | // })), 31 | // }); 32 | // } 33 | const members = await http.request< 34 | { database_name: string; database_id: string }[] 35 | >({ 36 | method: "GET", 37 | authorization, 38 | path: ["v2", "teams", options.id!], 39 | }); 40 | if (options.json) { 41 | console.log(JSON.stringify(members, null, 2)); 42 | return; 43 | } 44 | 45 | members.forEach((member) => { 46 | console.log( 47 | cliffy.Table.from( 48 | Object.entries(member).map(([k, v]) => [k.toString(), v.toString()]), 49 | ).toString(), 50 | ); 51 | console.log(); 52 | }); 53 | console.log(); 54 | console.log(); 55 | }); 56 | -------------------------------------------------------------------------------- /src/commands/redis/reset_password.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | import type { Database } from "./types.ts"; 7 | 8 | export const resetPasswordCmd = new Command() 9 | .name("reset-password") 10 | .description("reset the password of a redis database") 11 | .option("--id=", "The id of your database", { required: true }) 12 | .example( 13 | "Reset", 14 | `upstash redis reset-password --id=f860e7e2-27b8-4166-90d5-ea41e90b4809`, 15 | ) 16 | .action(async (options): Promise => { 17 | const authorization = await parseAuth(options); 18 | 19 | // if (!options.id) { 20 | // if (options.ci) { 21 | // throw new cliffy.ValidationError("id"); 22 | // } 23 | // const dbs = await http.request< 24 | // { database_name: string; database_id: string }[] 25 | // >({ 26 | // method: "GET", 27 | // authorization, 28 | // path: ["v2", "redis", "databases"], 29 | // }); 30 | // options.id = await cliffy.Select.prompt({ 31 | // message: "Select a database to delete", 32 | // options: dbs.map(({ database_name, database_id }) => ({ 33 | // name: database_name, 34 | // value: database_id, 35 | // })), 36 | // }); 37 | // } 38 | 39 | const db = await http.request({ 40 | method: "POST", 41 | authorization, 42 | path: ["v2", "redis", "reset-password", options.id!], 43 | }); 44 | if (options.json) { 45 | console.log(JSON.stringify(db, null, 2)); 46 | return; 47 | } 48 | console.log(cliffy.colors.brightGreen("Database password has been reset")); 49 | console.log(); 50 | console.log( 51 | cliffy.Table.from( 52 | Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), 53 | ).toString(), 54 | ); 55 | }); 56 | -------------------------------------------------------------------------------- /src/commands/team/remove_member.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | export const removeMemberCmd = new Command() 7 | .name("remove-member") 8 | .description("Remove a member from a team") 9 | .option("--id=", "The uuid of the team") 10 | .option("--email=", "The email of the member") 11 | .example( 12 | "Remove", 13 | `upstash team remove-member f860e7e2-27b8-4166-90d5-ea41e90b4809 f860e7e2-27b8-4166-90d5-ea41e90b4809`, 14 | ) 15 | .action(async (options): Promise => { 16 | const authorization = await parseAuth(options); 17 | 18 | // if (!options.id) { 19 | // if (options.ci) { 20 | // throw new cliffy.ValidationError("id"); 21 | // } 22 | // const teams = await http.request< 23 | // { team_name: string; team_id: string }[] 24 | // >({ 25 | // method: "GET", 26 | // authorization, 27 | // path: ["v2", "teams"], 28 | // }); 29 | // options.id = await cliffy.Select.prompt({ 30 | // message: "Select a team", 31 | // options: teams.map(({ team_name, team_id }) => ({ 32 | // name: team_name, 33 | // value: team_id, 34 | // })), 35 | // }); 36 | // } 37 | // if (!options.email) { 38 | // if (options.ci) { 39 | // throw new cliffy.ValidationError("email"); 40 | // } 41 | // options.email = await cliffy.Input.prompt("Enter the user's email"); 42 | // } 43 | 44 | const team = await http.request({ 45 | method: "DELETE", 46 | authorization, 47 | path: ["v2", "teams", "member"], 48 | body: { 49 | team_id: options.id, 50 | member_email: options.email, 51 | }, 52 | }); 53 | if (options.json) { 54 | console.log(JSON.stringify(team, null, 2)); 55 | return; 56 | } 57 | console.log(cliffy.colors.brightGreen("Member has been removed")); 58 | }); 59 | -------------------------------------------------------------------------------- /src/commands/redis/rename.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | export const renameCmd = new Command() 6 | .name("rename") 7 | .description("Rename a redis database") 8 | .option("--id=", "The id of your database", { required: true }) 9 | .option("--name=", "Choose a new name", { required: true }) 10 | .example( 11 | "Rename", 12 | `upstash redis rename f860e7e2-27b8-4166-90d5-ea41e90b4809`, 13 | ) 14 | .action(async (options): Promise => { 15 | const authorization = await parseAuth(options); 16 | 17 | // if (!options.id) { 18 | // if (options.ci) { 19 | // throw new cliffy.ValidationError("id"); 20 | // } 21 | // const dbs = await http.request< 22 | // { database_name: string; database_id: string }[] 23 | // >({ 24 | // method: "GET", 25 | // authorization, 26 | // path: ["v2", "redis", "databases"], 27 | // }); 28 | // options.id = await cliffy.Select.prompt({ 29 | // message: "Select a database to rename", 30 | // options: dbs.map(({ database_name, database_id }) => ({ 31 | // name: database_name, 32 | // value: database_id, 33 | // })), 34 | // }); 35 | // } 36 | 37 | // if (!options.name) { 38 | // if (options.ci) { 39 | // throw new cliffy.ValidationError("id"); 40 | // } 41 | // options.name = await cliffy.Input.prompt({ 42 | // message: "Choose a new name", 43 | // }); 44 | // } 45 | const db = await http.request({ 46 | method: "POST", 47 | authorization, 48 | path: ["v2", "redis", "rename", options.id!], 49 | body: { 50 | name: options.name, 51 | }, 52 | }); 53 | if (options.json) { 54 | console.log(JSON.stringify(db, null, 2)); 55 | return; 56 | } 57 | console.log(cliffy.colors.brightGreen("Database has been renamed")); 58 | }); 59 | -------------------------------------------------------------------------------- /src/commands/auth/login.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../util/command.ts"; 2 | import { DEFAULT_CONFIG_PATH, loadConfig } from "../../config.ts"; 3 | import { cliffy } from "../../deps.ts"; 4 | export const loginCmd = new Command() 5 | .name("login") 6 | .description( 7 | `Log into your upstash account. 8 | This will store your email and api key in ${DEFAULT_CONFIG_PATH}. 9 | you can override this with "--config=/path/to/.upstash.json"`, 10 | ) 11 | .option("-e, --email=", "The email you use in upstash console") 12 | .option( 13 | "-k, --api-key=", 14 | "Management api apiKey from https://console.upstash.com/account/api", 15 | ) 16 | .action((options): void => { 17 | const config = loadConfig(options.config); 18 | if (config) { 19 | throw new Error( 20 | `You are already logged in, please log out first or delete ${options.config}`, 21 | ); 22 | } 23 | let email = options.upstashEmail; 24 | if (!email) { 25 | email = options.email; 26 | } 27 | 28 | if (!email) { 29 | throw new cliffy.ValidationError( 30 | `email is missing, either use "--email" or set "UPSTASH_EMAIL" environment variable`, 31 | ); 32 | } 33 | // if (!email) { 34 | // if (options.ci) { 35 | // throw new cliffy.ValidationError("email"); 36 | // } 37 | // email = await cliffy.Input.prompt("Enter your email"); 38 | // } 39 | 40 | let apiKey = options.upstashApiKey; 41 | 42 | if (!apiKey) { 43 | apiKey = options.apiKey; 44 | } 45 | if (!apiKey) { 46 | throw new cliffy.ValidationError( 47 | `api key is missing, either use "--api-key" or set "UPSTASH_API_KEY" environment variable`, 48 | ); 49 | } 50 | // if (!apiKey) { 51 | // if (options.ci) { 52 | // throw new cliffy.ValidationError("apiKey"); 53 | // } 54 | // apiKey = await cliffy.Secret.prompt( 55 | // "Enter your apiKey from https://console.upstash.com/account/api" 56 | // ); 57 | // } 58 | 59 | Deno.writeTextFileSync(options.config, JSON.stringify({ email, apiKey })); 60 | }); 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | npm: 10 | name: Release on npm 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Set env 17 | run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 16 23 | 24 | - run: curl -fsSL https://deno.land/x/install/install.sh | sh 25 | 26 | - run: echo "$HOME/.deno/bin" > $GITHUB_PATH 27 | 28 | - name: Build 29 | run: deno run -A ./cmd/build.ts $VERSION 30 | 31 | - name: Publish 32 | if: "!github.event.release.prerelease" 33 | working-directory: ./dist 34 | run: | 35 | echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" > .npmrc 36 | npm publish --access public 37 | 38 | - name: Publish release candidate 39 | if: "github.event.release.prerelease" 40 | working-directory: ./dist 41 | run: | 42 | echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" > .npmrc 43 | npm publish --access public --tag=next 44 | 45 | binaries: 46 | name: Release binary 47 | runs-on: ubuntu-latest 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | arch: 52 | [ 53 | "x86_64-unknown-linux-gnu", 54 | "x86_64-pc-windows-msvc", 55 | "x86_64-apple-darwin", 56 | "aarch64-apple-darwin", 57 | ] 58 | steps: 59 | - name: Checkout Repo 60 | uses: actions/checkout@v2 61 | 62 | - run: curl -fsSL https://deno.land/x/install/install.sh | sh 63 | - run: echo "$HOME/.deno/bin" > $GITHUB_PATH 64 | 65 | - name: compile 66 | run: deno compile --allow-env --allow-read --allow-write --allow-net --target=${{ matrix.arch }} --output=./bin/upstash_${{ matrix.arch}} ./src/mod.ts 67 | 68 | - name: upload 69 | run: gh release upload ${GITHUB_REF#refs/*/} ./bin/* 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | -------------------------------------------------------------------------------- /src/commands/redis/move_to_team.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | 6 | export const moveToTeamCmd = new Command() 7 | .name("move-to-team") 8 | .description("Move a redis database to another team") 9 | .option("--id=", "The id of your database", { required: true }) 10 | .option("--team-id=", "The id of a team", { required: true }) 11 | .example("Move", `upstash redis move-to-team`) 12 | .action(async (options): Promise => { 13 | const authorization = await parseAuth(options); 14 | 15 | // if (!options.id) { 16 | // if (options.ci) { 17 | // throw new cliffy.ValidationError("id"); 18 | // } 19 | // const dbs = await http.request< 20 | // { database_name: string; database_id: string }[] 21 | // >({ 22 | // method: "GET", 23 | // authorization, 24 | // path: ["v2", "redis", "databases"], 25 | // }); 26 | // options.id = await cliffy.Select.prompt({ 27 | // message: "Select a database to move", 28 | // options: dbs.map(({ database_name, database_id }) => ({ 29 | // name: database_name, 30 | // value: database_id, 31 | // })), 32 | // }); 33 | // } 34 | // if (!options.teamId) { 35 | // const teams = await http.request< 36 | // { team_name: string; team_id: string }[] 37 | // >({ 38 | // method: "GET", 39 | // authorization, 40 | // path: ["v2", "teams"], 41 | // }); 42 | // options.teamId = await cliffy.Select.prompt({ 43 | // message: "Select the new team", 44 | // options: teams.map(({ team_name, team_id }) => ({ 45 | // name: team_name, 46 | // value: team_id, 47 | // })), 48 | // }); 49 | // } 50 | 51 | const db = await http.request({ 52 | method: "POST", 53 | authorization, 54 | path: ["v2", "redis", "move-to-team"], 55 | body: { 56 | database_id: options.id, 57 | team_id: options.teamId, 58 | }, 59 | }); 60 | if (options.json) { 61 | console.log(JSON.stringify(db, null, 2)); 62 | return; 63 | } 64 | console.log(cliffy.colors.brightGreen("Database has been moved")); 65 | }); 66 | -------------------------------------------------------------------------------- /src/commands/team/add_member.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | enum Role { 6 | admin = "admin", 7 | dev = "dev", 8 | finance = "finance", 9 | } 10 | export const addMemberCmd = new Command() 11 | .name("add-member") 12 | .description("Add a new member to a team") 13 | .type("role", new cliffy.EnumType(Role)) 14 | .option("--id ", "The id of your team", { required: true }) 15 | .option( 16 | "--member-email ", 17 | "The email of a user you want to add.", 18 | { 19 | required: true, 20 | }, 21 | ) 22 | .option("--role ", "The role for the new user", { 23 | required: true, 24 | }) 25 | .example( 26 | "Add new developer", 27 | `upstash team add-member --id=f860e7e2-27b8-4166-90d5-ea41e90b4809 --member-email=bob@acme.com --role=${Role.dev}`, 28 | ) 29 | .action(async (options): Promise => { 30 | const authorization = await parseAuth(options); 31 | 32 | if (!options.id) { 33 | if (options.ci) { 34 | throw new cliffy.ValidationError("id"); 35 | } 36 | const teams = await http.request< 37 | { team_name: string; team_id: string }[] 38 | >({ 39 | method: "GET", 40 | authorization, 41 | path: ["v2", "teams"], 42 | }); 43 | options.id = await cliffy.Select.prompt({ 44 | message: "Select a team", 45 | options: teams.map(({ team_name, team_id }) => ({ 46 | name: team_name, 47 | value: team_id, 48 | })), 49 | }); 50 | } 51 | if (!options.memberEmail) { 52 | if (options.ci) { 53 | throw new cliffy.ValidationError("memberEmail"); 54 | } 55 | options.memberEmail = await cliffy.Input.prompt("Enter the user's email"); 56 | } 57 | if (!options.role) { 58 | if (options.ci) { 59 | throw new cliffy.ValidationError("role"); 60 | } 61 | options.role = (await cliffy.Select.prompt({ 62 | message: "Select a role", 63 | options: Object.entries(Role).map(([name, value]) => ({ 64 | name, 65 | value, 66 | })), 67 | })) as Role; 68 | } 69 | 70 | const res = await http.request<{ 71 | team_name: string; 72 | member_email: string; 73 | member_role: Role; 74 | }>({ 75 | method: "POST", 76 | authorization, 77 | path: ["v2", "teams", "member"], 78 | body: { 79 | team_id: options.id, 80 | member_email: options.memberEmail, 81 | member_role: options.role, 82 | }, 83 | }); 84 | if (options.json) { 85 | console.log(JSON.stringify(res, null, 2)); 86 | return; 87 | } 88 | console.log( 89 | cliffy.colors.brightGreen( 90 | `${res.member_email} has been invited to join ${res.team_name} as ${res.member_role}`, 91 | ), 92 | ); 93 | }); 94 | -------------------------------------------------------------------------------- /src/commands/redis/create.ts: -------------------------------------------------------------------------------- 1 | import { cliffy } from "../../deps.ts"; 2 | import { Command } from "../../util/command.ts"; 3 | import { parseAuth } from "../../util/auth.ts"; 4 | import { http } from "../../util/http.ts"; 5 | import type { Database } from "./types.ts"; 6 | export enum Region { 7 | "us-west-1" = "us-west-1", 8 | "us-west-2" = "us-west-2", 9 | "us-east-1" = "us-east-1", 10 | "us-central1" = "us-central1", 11 | 12 | "eu-west-1" = "eu-west-1", 13 | "eu-central-1" = "eu-central-1", 14 | 15 | "ap-south-1" = "ap-south-1", 16 | "ap-southeast-1" = "ap-southeast-1", 17 | "ap-southeast-2" = "ap-southeast-2", 18 | 19 | "ap-northeast-1" = "ap-northeast-1", 20 | 21 | "sa-east-1" = "sa-east-1", 22 | } 23 | 24 | export const createCmd = new Command() 25 | .name("create") 26 | .description("Create a new redis database") 27 | .option("-n --name ", "Name of the database", { required: true }) 28 | .type("region", new cliffy.EnumType(Region)) 29 | .option("-r --region ", "Primary region of the database", { 30 | required: true, 31 | }) 32 | .option( 33 | "--read-regions [regions...:region]", 34 | "Read regions of the database", 35 | { 36 | required: false, 37 | }, 38 | ) 39 | .example("global", "upstash redis create --name mydb --region=us-east-1") 40 | .example( 41 | "with replication", 42 | "upstash redis create --name mydb --region=us-east-1 --read-regions=us-west-1 eu-west-1", 43 | ) 44 | .action(async (options): Promise => { 45 | const authorization = await parseAuth(options); 46 | 47 | // if (!options.name) { 48 | // if (options.ci) { 49 | // throw new cliffy.ValidationError("name"); 50 | // } 51 | // options.name = await cliffy.Input.prompt("Set a name for your database"); 52 | // } 53 | 54 | // if (!options.region) { 55 | // if (options.ci) { 56 | // throw new cliffy.ValidationError("region"); 57 | // } 58 | // options.region = (await cliffy.Select.prompt({ 59 | // message: "Select a region", 60 | // options: Object.entries(Region).map(([name, value]) => ({ 61 | // name, 62 | // value, 63 | // })), 64 | // })) as Region; 65 | // } 66 | const body: Record< 67 | string, 68 | string | number | boolean | undefined | string[] 69 | > = { 70 | name: options.name, 71 | region: "global", 72 | primary_region: options.region, 73 | read_regions: options.readRegions, 74 | }; 75 | 76 | if (body.read_regions !== undefined && !Array.isArray(body.read_regions)) { 77 | throw new Error("--read_regions should be an array"); 78 | } 79 | 80 | const db = await http.request({ 81 | method: "POST", 82 | authorization, 83 | path: ["v2", "redis", "database"], 84 | body, 85 | }); 86 | if (options.json) { 87 | console.log(JSON.stringify(db, null, 2)); 88 | return; 89 | } 90 | console.log(cliffy.colors.brightGreen("Database has been created")); 91 | console.log(); 92 | console.log( 93 | cliffy.Table.from( 94 | Object.entries(db).map(([k, v]) => [k.toString(), v.toString()]), 95 | ).toString(), 96 | ); 97 | console.log(); 98 | console.log(); 99 | 100 | console.log( 101 | "You can visit your database details page: " + 102 | cliffy.colors.yellow( 103 | "https://console.upstash.com/redis/" + db.database_id, 104 | ), 105 | ); 106 | console.log(); 107 | console.log( 108 | "Connect to your database with redis-cli: " + 109 | cliffy.colors.yellow( 110 | `redis-cli --tls -u redis://${db.password}@${db.endpoint}:${db.port}`, 111 | ), 112 | ); 113 | }); 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upstash CLI 2 | 3 | Manage Upstash resources in your terminal or CI. 4 | 5 | ![](./img/banner.svg) 6 | 7 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/upstash/cli) 8 | [![Downloads/week](https://img.shields.io/npm/dw/lstr.svg)](https://npmjs.org/package/@upstash/cli) 9 | 10 | # Installation 11 | 12 | ## npm 13 | 14 | You can install upstash's cli directly from npm 15 | 16 | ```bash 17 | npm i -g @upstash/cli 18 | ``` 19 | 20 | It will be added as `upstash` to your system's path. 21 | 22 | ## Compiled binaries: 23 | 24 | `upstash` is also available from the 25 | [releases page](https://github.com/upstash/cli/releases/latest) compiled for 26 | windows, linux and mac (both intel and m1). 27 | 28 | # Usage 29 | 30 | ```bash 31 | > upstash 32 | 33 | Usage: upstash 34 | Version: development 35 | 36 | Description: 37 | 38 | Official cli for Upstash products 39 | 40 | Options: 41 | 42 | -h, --help - Show this help. 43 | -V, --version - Show the version number for this program. 44 | -c, --config - Path to .upstash.json file 45 | 46 | Commands: 47 | 48 | auth - Login and logout 49 | redis - Manage redis database instances 50 | team - Manage your teams and their members 51 | 52 | Environment variables: 53 | 54 | UPSTASH_EMAIL - The email you use on upstash 55 | UPSTASH_API_KEY - The api key from upstash 56 | ``` 57 | 58 | ## Authentication 59 | 60 | When running `upstash` for the first time, you should log in using 61 | `upstash auth login`. Provide your email and an api key. 62 | [See here for how to get a key.](https://docs.upstash.com/redis/howto/developerapi#api-development) 63 | 64 | As an alternative to logging in, you can provide `UPSTASH_EMAIL` and 65 | `UPSTASH_API_KEY` as environment variables. 66 | 67 | ## Usage 68 | 69 | Let's create a new redis database: 70 | 71 | ``` 72 | > upstash redis create --name=my-db --region=eu-west-1 73 | Database has been created 74 | 75 | database_id a3e25299-132a-45b9-b026-c73f5a807859 76 | database_name my-db 77 | database_type Pay as You Go 78 | region eu-west-1 79 | type paid 80 | port 37090 81 | creation_time 1652687630 82 | state active 83 | password 88ae6392a1084d1186a3da37fb5f5a30 84 | user_email andreas@upstash.com 85 | endpoint eu1-magnetic-lacewing-37090.upstash.io 86 | edge false 87 | multizone false 88 | rest_token AZDiASQgYTNlMjUyOTktMTMyYS00NWI5LWIwMjYtYzczZjVhODA3ODU5ODhhZTYzOTJhMTA4NGQxMTg2YTNkYTM3ZmI1ZjVhMzA= 89 | read_only_rest_token ApDiASQgYTNlMjUyOTktMTMyYS00NWI5LWIwMjYtYzczZjVhODA3ODU5O_InFjRVX1XHsaSjq1wSerFCugZ8t8O1aTfbF6Jhq1I= 90 | 91 | 92 | You can visit your database details page: https://console.upstash.com/redis/a3e25299-132a-45b9-b026-c73f5a807859 93 | 94 | Connect to your database with redis-cli: redis-cli -u redis://88ae6392a1084d1186a3da37fb5f5a30@eu1-magnetic-lacewing-37090.upstash.io:37090 95 | ``` 96 | 97 | ## Output 98 | 99 | Most commands support the `--json` flag to return the raw api response as json, 100 | which you can parse and automate your system. 101 | 102 | ```bash 103 | > upstash redis create --name=test2113 --region=us-central1 --json | jq '.endpoint' 104 | 105 | "gusc1-clean-gelding-30208.upstash.io" 106 | ``` 107 | 108 | ## Contributing 109 | 110 | If anything feels wrong, you discover a bug or want to request improvements, 111 | please create an issue or talk to us on 112 | [Discord](https://discord.com/invite/w9SenAtbme) 113 | --------------------------------------------------------------------------------