Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ upstash start-redis

This prints markdown with credentials and a quickstart. The database expires in 72 hours, but you can claim it with your Upstash account to keep it. Pass `--id <database-id>` to re-fetch the credentials of a database you created earlier. Every command that touches your Upstash account needs authentication — only `start-redis` and the `login`/`logout` credential helpers do not.

If you are an agent, identify yourself with `--user-agent` on every `start-redis` call, so we can see which agents are creating databases:

```bash
upstash start-redis --user-agent claude-code # or cursor, codex, opencode, ...
```

## Authentication

Grab a Developer API key from the [Upstash Console](https://console.upstash.com/account/api), then save it once per machine:
Expand Down
16 changes: 14 additions & 2 deletions src/commands/start-redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from "commander";
import { plainError } from "../output.js";

const START_REDIS_URL = "https://upstash.com/start-redis";
const DEFAULT_USER_AGENT = "upstash/cli";

export function registerStartRedis(program: Command): void {
program
Expand All @@ -10,15 +11,26 @@ export function registerStartRedis(program: Command): void {
"Get a free temporary Redis database — no account or API key needed. Prints markdown with credentials and a quickstart. Expires in 72 hours; claim it with an Upstash account to keep it.",
)
.option("--id <id>", "Re-fetch the credentials of a database created earlier")
.action(async (flags: { id?: string }) => {
.option(
"--user-agent <name>",
"Identify the caller. If you are an agent, pass your own name (e.g. claude-code, cursor, codex, opencode)",
)
.action(async (flags: { id?: string; userAgent?: string }) => {
const headers: Record<string, string> = {
"User-Agent": flags.userAgent || DEFAULT_USER_AGENT,
};
if (flags.id) {
headers["Idempotency-Key"] = flags.id;
}

// This command's output is markdown, not JSON, so network failures are
// reported as plain text too rather than through the JSON error path.
let response: Response;
let text: string;
try {
response = await fetch(START_REDIS_URL, {
method: "POST",
headers: flags.id ? { "Idempotency-Key": flags.id } : undefined,
headers,
});
text = await response.text();
} catch (err) {
Expand Down
4 changes: 0 additions & 4 deletions tests/integration/redis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@ describe("redis rename", () => {
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]);
});
});

Expand Down
3 changes: 0 additions & 3 deletions tests/integration/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ describe("search rename", () => {
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]);
});
});

Expand Down
3 changes: 0 additions & 3 deletions tests/integration/vector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ describe("vector rename", () => {
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]);
});
});

Expand Down
32 changes: 30 additions & 2 deletions tests/unit/start-redis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("start-redis", () => {
const [url, init] = fetchMock.mock.calls[0]!;
expect(url).toBe("https://upstash.com/start-redis");
expect(init?.method).toBe("POST");
expect(init?.headers).toBeUndefined();
expect(init?.headers).toEqual({ "User-Agent": "upstash/cli" });
expect(output).toEqual([MARKDOWN.trimEnd()]);
});

Expand All @@ -50,7 +50,35 @@ describe("start-redis", () => {
await run(["start-redis", "--id", "db-123"]);

const [, init] = fetchMock.mock.calls[0]!;
expect(init?.headers).toEqual({ "Idempotency-Key": "db-123" });
expect(init?.headers).toEqual({
"Idempotency-Key": "db-123",
"User-Agent": "upstash/cli",
});
});

it("sends the caller name as a user agent when --user-agent is given", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(new Response(MARKDOWN, { status: 200 }));

await run(["start-redis", "--user-agent", "claude-code"]);

const [, init] = fetchMock.mock.calls[0]!;
expect(init?.headers).toEqual({ "User-Agent": "claude-code" });
});

it("sends both headers when --id and --user-agent are given", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockResolvedValue(new Response(MARKDOWN, { status: 200 }));

await run(["start-redis", "--id", "db-123", "--user-agent", "cursor"]);

const [, init] = fetchMock.mock.calls[0]!;
expect(init?.headers).toEqual({
"Idempotency-Key": "db-123",
"User-Agent": "cursor",
});
});

it("throws a plain error when the network is unreachable", async () => {
Expand Down
Loading