From e4d77cb9c43d4824e04782badef868fc490a9877 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 12:15:47 +0000 Subject: [PATCH 1/3] Run setup for a bare `upstash` when published under the short name The CLI will also be published as the unscoped `upstash` package, from the same build with the same version, so `npx upstash` works. Under that name a bare `upstash` in a terminal runs `upstash setup`; with arguments it is the full CLI. Outside a terminal a bare `upstash` keeps printing the help, so an agent or script never rewrites agent configs by accident. @upstash/cli is unchanged. A second publish rather than a wrapper that depends on @upstash/cli: both packages would declare an `upstash` bin, and npm can link the dependency's bin instead of the wrapper's, which skips the setup default. The release workflow change that publishes the second package is in the PR description, to be added by someone with workflow permissions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LWUubWwYWp1dZTK1S9Ek95 --- README.md | 8 ++++++-- src/cli.ts | 7 +++++-- src/short-name.ts | 13 +++++++++++++ tests/unit/short-name.test.ts | 21 +++++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/short-name.ts create mode 100644 tests/unit/short-name.test.ts diff --git a/README.md b/README.md index e09b9b6..1397f71 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,11 @@ Agent-friendly CLI for managing & debugging Upstash resources from your terminal Requires Node.js 20 or newer. ```bash -npm i -g @upstash/cli +npm i -g upstash ``` +`upstash` and `@upstash/cli` are the same CLI, published from the same build with the same version. The only difference: a bare `upstash` in a terminal runs `upstash setup`. + For agents, pair the CLI with the [Upstash Skill](https://docs.upstash.com/agent-resources/skills), it bundles Upstash docs alongside docs for the `upstash` CLI. ```bash @@ -24,9 +26,11 @@ npx skills add upstash/skills One command wires Upstash into your coding agents: ```bash -npx @upstash/cli setup +npx upstash ``` +With no arguments in a terminal, `upstash` runs `upstash setup`. From an agent or a script, a bare `upstash` prints the help instead, so run `upstash setup` explicitly there. + It picks the best install path per agent: | Agent | What gets installed | diff --git a/src/cli.ts b/src/cli.ts index 881703a..14d6711 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command } from "commander"; import pkg from "../package.json" with { type: "json" }; -const { version } = pkg; +const { name, version } = pkg; import { registerRedis } from "./commands/redis/index.js"; import { registerTeam } from "./commands/team/index.js"; import { registerVector } from "./commands/vector/index.js"; @@ -14,6 +14,7 @@ import { registerStartRedis } from "./commands/start-redis.js"; import { registerTelemetry } from "./commands/telemetry.js"; import { registerSetup } from "./commands/setup.js"; import { handleError } from "./output.js"; +import { withDefaultCommand } from "./short-name.js"; import dotenv from "dotenv"; // Pre-scan argv for --env-path before Commander parses, so dotenv loads @@ -53,4 +54,6 @@ registerSearch(program); registerQStash(program); registerBlob(program); -program.parseAsync().catch(handleError); +const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY); +const args = withDefaultCommand(name, process.argv.slice(2), interactive); +program.parseAsync(args, { from: "user" }).catch(handleError); diff --git a/src/short-name.ts b/src/short-name.ts new file mode 100644 index 0000000..99598ba --- /dev/null +++ b/src/short-name.ts @@ -0,0 +1,13 @@ +/** + * The CLI is published twice from the same build: as `@upstash/cli` and as + * `upstash`, so `npx upstash` works. Under the short name, a bare `upstash` + * in a terminal is the agent setup entry point. Outside a terminal (an agent, + * a script, CI) it keeps the default help output, so it never rewrites agent + * configs unasked. + */ +export const SHORT_NAME = "upstash"; + +export function withDefaultCommand(packageName: string, args: string[], interactive: boolean): string[] { + if (packageName === SHORT_NAME && args.length === 0 && interactive) return ["setup"]; + return args; +} diff --git a/tests/unit/short-name.test.ts b/tests/unit/short-name.test.ts new file mode 100644 index 0000000..46da793 --- /dev/null +++ b/tests/unit/short-name.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { withDefaultCommand } from "../../src/short-name.js"; + +describe("the `upstash` short name", () => { + it("runs setup for a bare `upstash` in a terminal", () => { + expect(withDefaultCommand("upstash", [], true)).toEqual(["setup"]); + }); + + it("keeps the help for a bare `upstash` outside a terminal, so agents never trigger setup", () => { + expect(withDefaultCommand("upstash", [], false)).toEqual([]); + }); + + it("passes every other invocation through unchanged", () => { + expect(withDefaultCommand("upstash", ["redis", "list"], true)).toEqual(["redis", "list"]); + expect(withDefaultCommand("upstash", ["--version"], true)).toEqual(["--version"]); + }); + + it("leaves @upstash/cli as it is", () => { + expect(withDefaultCommand("@upstash/cli", [], true)).toEqual([]); + }); +}); From 8f4a2a366ebfb1be40d4d004988cc5aca0e1b415 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 12:33:29 +0000 Subject: [PATCH 2/3] Make `upstash` the package name; a bare command in a terminal runs setup `upstash` is now the primary package name. The release workflow publishes it, then the same build again as @upstash/cli, so existing installs keep getting releases and the old name can be deprecated later with one command. With both names running the same code, the name check is gone: a bare `upstash` in a terminal runs `upstash setup` under either name, and outside a terminal it still prints the help. The telemetry header keeps reporting @upstash/cli so the analytics series stays continuous. The release workflow change ships separately (it needs workflow permissions). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LWUubWwYWp1dZTK1S9Ek95 --- README.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- src/cli.ts | 6 +++--- src/default-command.ts | 9 +++++++++ src/short-name.ts | 13 ------------- tests/unit/default-command.test.ts | 17 +++++++++++++++++ tests/unit/short-name.test.ts | 21 --------------------- 8 files changed, 34 insertions(+), 42 deletions(-) create mode 100644 src/default-command.ts delete mode 100644 src/short-name.ts create mode 100644 tests/unit/default-command.test.ts delete mode 100644 tests/unit/short-name.test.ts diff --git a/README.md b/README.md index 1397f71..0d9363e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Upstash CLI [![GitHub release](https://img.shields.io/github/v/release/upstash/cli)](https://github.com/upstash/cli/releases/latest) -[![npm downloads](https://img.shields.io/npm/dw/@upstash/cli.svg)](https://npmjs.org/package/@upstash/cli) +[![npm downloads](https://img.shields.io/npm/dw/upstash.svg)](https://npmjs.org/package/upstash) Agent-friendly CLI for managing & debugging Upstash resources from your terminal. [Docs](https://upstash.com/docs/agent-resources/cli). @@ -13,7 +13,7 @@ Requires Node.js 20 or newer. npm i -g upstash ``` -`upstash` and `@upstash/cli` are the same CLI, published from the same build with the same version. The only difference: a bare `upstash` in a terminal runs `upstash setup`. +The same build is also published as `@upstash/cli`, with the same version, so existing installs keep working. For agents, pair the CLI with the [Upstash Skill](https://docs.upstash.com/agent-resources/skills), it bundles Upstash docs alongside docs for the `upstash` CLI. diff --git a/package-lock.json b/package-lock.json index ada0576..4d2dd4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@upstash/cli", + "name": "upstash", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@upstash/cli", + "name": "upstash", "version": "0.0.0", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ed3cf19..7ba2027 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@upstash/cli", + "name": "upstash", "version": "0.0.0", "description": "Agent-friendly CLI for Upstash", "repository": { diff --git a/src/cli.ts b/src/cli.ts index 14d6711..6f8a824 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command } from "commander"; import pkg from "../package.json" with { type: "json" }; -const { name, version } = pkg; +const { version } = pkg; import { registerRedis } from "./commands/redis/index.js"; import { registerTeam } from "./commands/team/index.js"; import { registerVector } from "./commands/vector/index.js"; @@ -14,7 +14,7 @@ import { registerStartRedis } from "./commands/start-redis.js"; import { registerTelemetry } from "./commands/telemetry.js"; import { registerSetup } from "./commands/setup.js"; import { handleError } from "./output.js"; -import { withDefaultCommand } from "./short-name.js"; +import { withDefaultCommand } from "./default-command.js"; import dotenv from "dotenv"; // Pre-scan argv for --env-path before Commander parses, so dotenv loads @@ -55,5 +55,5 @@ registerQStash(program); registerBlob(program); const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY); -const args = withDefaultCommand(name, process.argv.slice(2), interactive); +const args = withDefaultCommand(process.argv.slice(2), interactive); program.parseAsync(args, { from: "user" }).catch(handleError); diff --git a/src/default-command.ts b/src/default-command.ts new file mode 100644 index 0000000..a84f4af --- /dev/null +++ b/src/default-command.ts @@ -0,0 +1,9 @@ +/** + * A bare `upstash` in a terminal is the agent setup entry point, so + * `npx upstash` connects your coding agents. Outside a terminal (an agent, a + * script, CI) it keeps the default help output, so it never rewrites agent + * configs unasked. + */ +export function withDefaultCommand(args: string[], interactive: boolean): string[] { + return args.length === 0 && interactive ? ["setup"] : args; +} diff --git a/src/short-name.ts b/src/short-name.ts deleted file mode 100644 index 99598ba..0000000 --- a/src/short-name.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * The CLI is published twice from the same build: as `@upstash/cli` and as - * `upstash`, so `npx upstash` works. Under the short name, a bare `upstash` - * in a terminal is the agent setup entry point. Outside a terminal (an agent, - * a script, CI) it keeps the default help output, so it never rewrites agent - * configs unasked. - */ -export const SHORT_NAME = "upstash"; - -export function withDefaultCommand(packageName: string, args: string[], interactive: boolean): string[] { - if (packageName === SHORT_NAME && args.length === 0 && interactive) return ["setup"]; - return args; -} diff --git a/tests/unit/default-command.test.ts b/tests/unit/default-command.test.ts new file mode 100644 index 0000000..599b9d9 --- /dev/null +++ b/tests/unit/default-command.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { withDefaultCommand } from "../../src/default-command.js"; + +describe("bare `upstash`", () => { + it("runs setup in a terminal", () => { + expect(withDefaultCommand([], true)).toEqual(["setup"]); + }); + + it("keeps the help outside a terminal, so agents never trigger setup", () => { + expect(withDefaultCommand([], false)).toEqual([]); + }); + + it("passes every other invocation through unchanged", () => { + expect(withDefaultCommand(["redis", "list"], true)).toEqual(["redis", "list"]); + expect(withDefaultCommand(["--version"], true)).toEqual(["--version"]); + }); +}); diff --git a/tests/unit/short-name.test.ts b/tests/unit/short-name.test.ts deleted file mode 100644 index 46da793..0000000 --- a/tests/unit/short-name.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { withDefaultCommand } from "../../src/short-name.js"; - -describe("the `upstash` short name", () => { - it("runs setup for a bare `upstash` in a terminal", () => { - expect(withDefaultCommand("upstash", [], true)).toEqual(["setup"]); - }); - - it("keeps the help for a bare `upstash` outside a terminal, so agents never trigger setup", () => { - expect(withDefaultCommand("upstash", [], false)).toEqual([]); - }); - - it("passes every other invocation through unchanged", () => { - expect(withDefaultCommand("upstash", ["redis", "list"], true)).toEqual(["redis", "list"]); - expect(withDefaultCommand("upstash", ["--version"], true)).toEqual(["--version"]); - }); - - it("leaves @upstash/cli as it is", () => { - expect(withDefaultCommand("@upstash/cli", [], true)).toEqual([]); - }); -}); From 46349113f159b24f40d11551784ecd0ca41da4d0 Mon Sep 17 00:00:00 2001 From: Cahid Arda Date: Mon, 5 Oct 2026 15:36:54 +0300 Subject: [PATCH 3/3] Update release.yml --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d18d2b..ceb1d96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,8 @@ jobs: - run: npm run build + # Publishes `upstash` (the name in package.json), then the same build + # again as @upstash/cli so existing installs keep getting releases. - name: Publish release candidate if: github.event.release.prerelease run: npm publish --access public --tag=canary --provenance @@ -38,3 +40,16 @@ jobs: - name: Publish if: "!github.event.release.prerelease" run: npm publish --access public --provenance + + - name: Rename the package to @upstash/cli + run: | + jq '.name = "@upstash/cli"' package.json > package.tmp.json + mv package.tmp.json package.json + + - name: Publish @upstash/cli release candidate + if: github.event.release.prerelease + run: npm publish --access public --tag=canary --provenance + + - name: Publish @upstash/cli + if: "!github.event.release.prerelease" + run: npm publish --access public --provenance