diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 8f2acaca814..9ce857780b4 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,9 +1,15 @@ -name: Build CLI Distribution +name: Build CLI + +# Reusable build workflow. Callers (ci-cli.yml, release-cli.yml) pass a +# matrix as JSON to control which targets to build. on: - push: - tags: ["cli-v*"] - workflow_dispatch: + workflow_call: + inputs: + targets: + description: 'JSON array of {os, target} build matrix entries' + required: true + type: string jobs: build: @@ -11,12 +17,7 @@ jobs: strategy: fail-fast: false matrix: - include: - - os: macos-14 - target: darwin-arm64 - - os: ubuntu-latest - target: linux-x64 - + include: ${{ fromJSON(inputs.targets) }} runs-on: ${{ matrix.os }} steps: - name: Checkout code @@ -32,48 +33,48 @@ jobs: with: node-version: 22 - - name: Install dependencies + - name: Install dependencies (Linux — skip scripts, rebuild manually) + if: startsWith(matrix.target, 'linux-') + run: | + # Bun's install-script runner has a cache materialization race + # against node-pty's compile-from-source path on Linux (node-pty + # ships no Linux prebuilds). Skip scripts at install, then rebuild + # native deps explicitly via npm/node-gyp. + set -euo pipefail + bun install --frozen --ignore-scripts + PTY_DIR=$(ls -d node_modules/.bun/node-pty@*/node_modules/node-pty) + (cd "$PTY_DIR" && npx --yes node-gyp rebuild) + npm rebuild better-sqlite3 + npm rebuild @parcel/watcher + + - name: Install dependencies (macOS) + if: startsWith(matrix.target, 'darwin-') run: bun install --frozen - name: Build distribution working-directory: packages/cli env: - RELAY_URL: https://relay.superset.sh - CLOUD_API_URL: https://api.superset.sh + RELAY_URL: ${{ secrets.RELAY_URL }} + SUPERSET_API_URL: ${{ vars.SUPERSET_API_URL }} + SUPERSET_WEB_URL: ${{ vars.SUPERSET_WEB_URL }} run: bun run build:dist --target=${{ matrix.target }} + - name: Smoke test binary + run: | + DIST="./packages/cli/dist/superset-${{ matrix.target }}" + "$DIST/bin/superset" --version + "$DIST/bin/superset" --help + "$DIST/lib/node" --version + NODE_PATH="$DIST/lib/node_modules" "$DIST/lib/node" -e ' + for (const m of ["better-sqlite3", "node-pty", "@parcel/watcher", "libsql"]) { + require(m); + console.log(m, "OK"); + } + ' + - name: Upload tarball uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: superset-${{ matrix.target }} path: packages/cli/dist/superset-${{ matrix.target }}.tar.gz if-no-files-found: error - - release: - name: Create GitHub Release - needs: build - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/cli-v') - permissions: - contents: write - - steps: - - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - - name: Download all artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - with: - path: release-artifacts - pattern: superset-* - merge-multiple: true - - - name: Create Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "${{ github.ref_name }}" \ - release-artifacts/*.tar.gz \ - --title "Superset CLI ${{ github.ref_name }}" \ - --generate-notes \ - --draft diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml index 42ce5f8fea4..cc40a65f742 100644 --- a/.github/workflows/bump-homebrew.yml +++ b/.github/workflows/bump-homebrew.yml @@ -39,7 +39,7 @@ jobs: TAG: ${{ steps.version.outputs.tag }} run: | set -euo pipefail - for target in darwin-arm64 linux-x64; do + for target in darwin-arm64 linux-x64 linux-arm64; do url="https://github.com/superset-sh/superset/releases/download/${TAG}/superset-${target}.tar.gz" echo "Fetching SHA for $url" tmp=$(mktemp) @@ -65,6 +65,7 @@ jobs: VERSION: ${{ steps.version.outputs.version }} DARWIN_ARM64_SHA: ${{ steps.shas.outputs.darwin_arm64_sha }} LINUX_X64_SHA: ${{ steps.shas.outputs.linux_x64_sha }} + LINUX_ARM64_SHA: ${{ steps.shas.outputs.linux_arm64_sha }} run: | set -euo pipefail python3 - <<'PYEOF' > homebrew-tap/Formula/superset.rb @@ -87,6 +88,10 @@ jobs: url "https://github.com/superset-sh/superset/releases/download/cli-v#{{version}}/superset-linux-x64.tar.gz" sha256 "{linux_x64}" end + on_arm do + url "https://github.com/superset-sh/superset/releases/download/cli-v#{{version}}/superset-linux-arm64.tar.gz" + sha256 "{linux_arm64}" + end end def install @@ -104,6 +109,7 @@ jobs: version=os.environ["VERSION"], darwin_arm64=os.environ["DARWIN_ARM64_SHA"], linux_x64=os.environ["LINUX_X64_SHA"], + linux_arm64=os.environ["LINUX_ARM64_SHA"], ), end="") PYEOF diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e2dc1ff84d..a74c1491f63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: - name: Test env: - RELAY_URL: https://relay.superset.sh + RELAY_URL: ${{ secrets.RELAY_URL }} run: bun run test typecheck: @@ -147,5 +147,34 @@ jobs: - name: Build Desktop env: - RELAY_URL: https://relay.superset.sh + RELAY_URL: ${{ secrets.RELAY_URL }} run: bun turbo run build --filter=@superset/desktop + + build-cli: + name: Build CLI + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Setup Bun + id: setup-bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Cache dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-revision }}-${{ hashFiles('bun.lock') }} + + - name: Install dependencies + run: bun install --frozen --ignore-scripts + + - name: Build CLI + env: + RELAY_URL: ${{ secrets.RELAY_URL }} + SUPERSET_API_URL: ${{ vars.SUPERSET_API_URL }} + SUPERSET_WEB_URL: ${{ vars.SUPERSET_WEB_URL }} + run: bun turbo run build --filter=@superset/cli diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 00000000000..08fff450891 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -0,0 +1,82 @@ +name: Release CLI + +# Fires on cli-v* tag push. Builds the full 3-target matrix and publishes +# a draft GitHub Release plus a rolling cli-latest pointer. workflow_dispatch +# is the manual escape hatch for testing the full pipeline without cutting +# a tag (the release job is gated to tag pushes only). + +on: + push: + tags: ["cli-v*"] + workflow_dispatch: + +jobs: + build: + uses: ./.github/workflows/build-cli.yml + with: + targets: '[{"os":"ubuntu-latest","target":"linux-x64"},{"os":"macos-14","target":"darwin-arm64"},{"os":"ubuntu-24.04-arm","target":"linux-arm64"}]' + secrets: inherit + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/cli-v') + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + + - name: Download all artifacts + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + path: release-artifacts + pattern: superset-* + merge-multiple: true + + - name: Create Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # --prerelease is a workaround: GitHub's /releases/latest endpoint + # doesn't filter by tag prefix, so a published non-prerelease cli-v* + # release would shadow desktop's auto-updater (which currently reads + # /releases/latest/download/latest-{mac,linux}.yml). Tracked in + # plans/release-channels-spec.md — drop once desktop migrates to a + # desktop-latest rolling pointer. + gh release create "${{ github.ref_name }}" \ + release-artifacts/*.tar.gz \ + --title "Superset CLI ${{ github.ref_name }}" \ + --generate-notes \ + --draft \ + --prerelease + + - name: Publish version manifest + env: + VERSION_TAG: ${{ github.ref_name }} + run: | + # Strip the `cli-v` prefix; the manifest carries just the semver. + echo "${VERSION_TAG#cli-v}" > release-artifacts/version.txt + + - name: Update rolling cli-latest release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION_TAG: ${{ github.ref_name }} + run: | + # `cli-latest` is a rolling tag/release that always points at the + # newest published CLI build. The update command fetches assets + # from `/releases/download/cli-latest/` so it never has to filter + # the repo's release list (where desktop releases would otherwise + # shadow the latest CLI release on the global /releases/latest + # endpoint). Recreate it on every CLI release. + set -euo pipefail + gh release delete cli-latest --yes --cleanup-tag || true + gh release create cli-latest \ + release-artifacts/*.tar.gz \ + release-artifacts/version.txt \ + --title "Latest Superset CLI" \ + --notes "Rolling pointer to the latest published CLI release. See [${VERSION_TAG}](https://github.com/${{ github.repository }}/releases/tag/${VERSION_TAG}) for changelog." \ + --target "${{ github.sha }}" \ + --prerelease diff --git a/apps/api/package.json b/apps/api/package.json index 06419d3d03a..2aed3e182b9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -25,6 +25,7 @@ "@superset/auth": "workspace:*", "@superset/db": "workspace:*", "@superset/mcp": "workspace:*", + "@superset/mcp-v2": "workspace:*", "@superset/shared": "workspace:*", "@superset/trpc": "workspace:*", "@t3-oss/env-nextjs": "^0.13.8", diff --git a/apps/api/src/app/api/v2/agent/[transport]/route.ts b/apps/api/src/app/api/v2/agent/[transport]/route.ts new file mode 100644 index 00000000000..89482a47659 --- /dev/null +++ b/apps/api/src/app/api/v2/agent/[transport]/route.ts @@ -0,0 +1,69 @@ +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { + createMcpServer, + isMcpUnauthorized, + type McpContext, + resolveMcpContext, +} from "@superset/mcp-v2"; +import { env } from "@/env"; +import { posthog } from "@/lib/analytics"; +import { getOAuthProtectedResourceMetadataUrl } from "@/lib/oauth-metadata"; + +function unauthorizedResponse(req: Request, message: string): Response { + return new Response( + JSON.stringify({ error: { code: "UNAUTHORIZED", message } }), + { + status: 401, + headers: { + "WWW-Authenticate": `Bearer realm="superset", resource_metadata="${getOAuthProtectedResourceMetadataUrl(req)}"`, + "Content-Type": "application/json", + }, + }, + ); +} + +async function handle(req: Request): Promise { + let ctx: McpContext; + try { + ctx = await resolveMcpContext(req, env.NEXT_PUBLIC_API_URL); + } catch (error) { + if (isMcpUnauthorized(error)) { + return unauthorizedResponse(req, error.message); + } + throw error; + } + + const server = createMcpServer({ + onToolCall: (event) => { + posthog.capture({ + distinctId: event.userId, + event: "mcp_tool_called", + properties: { + tool: event.toolName, + organization_id: event.organizationId, + auth_source: event.source, + client_label: event.clientLabel, + duration_ms: event.durationMs, + success: event.success, + error_message: event.errorMessage, + mcp_server: "superset-v2", + mcp_server_version: "0.1.0", + }, + groups: { organization: event.organizationId }, + }); + }, + }); + const transport = new WebStandardStreamableHTTPServerTransport(); + await server.connect(transport); + + return transport.handleRequest(req, { + authInfo: { + token: ctx.bearerToken, + clientId: ctx.source === "api-key" ? "api-key" : "oauth", + scopes: ["mcp:full"], + extra: { mcpContext: ctx }, + }, + }); +} + +export { handle as GET, handle as POST, handle as DELETE }; diff --git a/apps/desktop/HOST_SERVICE_BOUNDARIES.md b/apps/desktop/HOST_SERVICE_BOUNDARIES.md index 21340029654..a4acf53c3c4 100644 --- a/apps/desktop/HOST_SERVICE_BOUNDARIES.md +++ b/apps/desktop/HOST_SERVICE_BOUNDARIES.md @@ -356,7 +356,7 @@ import { LocalModelProvider } from "@superset/host-service/providers/desktop"; createApp({ config: { dbPath: path.join(orgDir, "host.db"), - cloudApiUrl: env.CLOUD_API_URL, + cloudApiUrl: env.SUPERSET_API_URL, migrationsPath: app.isPackaged ? path.join(process.resourcesPath, "resources/host-migrations") : path.join(app.getAppPath(), "../../packages/host-service/drizzle"), @@ -381,7 +381,7 @@ import { createApp, PskHostAuthProvider, JwtApiAuthProvider, createApp({ config: { dbPath: env.HOST_DB_PATH, - cloudApiUrl: env.CLOUD_API_URL, + cloudApiUrl: env.SUPERSET_API_URL, migrationsPath: join(import.meta.dirname, "../../drizzle"), allowedOrigins: env.CORS_ORIGINS, }, diff --git a/apps/desktop/src/main/host-service/env.ts b/apps/desktop/src/main/host-service/env.ts index dfe77059d67..7641208ca13 100644 --- a/apps/desktop/src/main/host-service/env.ts +++ b/apps/desktop/src/main/host-service/env.ts @@ -4,7 +4,7 @@ import { z } from "zod"; export const env = createEnv({ server: { AUTH_TOKEN: z.string().min(1), - CLOUD_API_URL: z.string().url(), + SUPERSET_API_URL: z.string().url(), HOST_DB_PATH: z.string().min(1), HOST_MIGRATIONS_FOLDER: z.string().min(1), HOST_SERVICE_SECRET: z.string().min(1), diff --git a/apps/desktop/src/main/host-service/index.ts b/apps/desktop/src/main/host-service/index.ts index b4b8983b206..75e9ca7a8c7 100644 --- a/apps/desktop/src/main/host-service/index.ts +++ b/apps/desktop/src/main/host-service/index.ts @@ -28,14 +28,14 @@ async function main(): Promise { const authProvider = new JwtApiAuthProvider( env.AUTH_TOKEN, - env.CLOUD_API_URL, + env.SUPERSET_API_URL, ); const { app, injectWebSocket, api } = createApp({ config: { organizationId: env.ORGANIZATION_ID, dbPath: env.HOST_DB_PATH, - cloudApiUrl: env.CLOUD_API_URL, + cloudApiUrl: env.SUPERSET_API_URL, migrationsFolder: env.HOST_MIGRATIONS_FOLDER, allowedOrigins: [ `http://localhost:${env.DESKTOP_VITE_PORT}`, diff --git a/apps/desktop/src/main/lib/host-service-coordinator.ts b/apps/desktop/src/main/lib/host-service-coordinator.ts index 779627d3e5f..a34826d516f 100644 --- a/apps/desktop/src/main/lib/host-service-coordinator.ts +++ b/apps/desktop/src/main/lib/host-service-coordinator.ts @@ -480,7 +480,7 @@ export class HostServiceCoordinator extends EventEmitter { SUPERSET_AGENT_HOOK_PORT: String(sharedEnv.DESKTOP_NOTIFICATIONS_PORT), SUPERSET_AGENT_HOOK_VERSION: HOOK_PROTOCOL_VERSION, AUTH_TOKEN: config.authToken, - CLOUD_API_URL: config.cloudApiUrl, + SUPERSET_API_URL: config.cloudApiUrl, }); // `getProcessEnvWithShellPath` merges in the user's interactive shell env, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/components/AutomationBody/AutomationBody.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/components/AutomationBody/AutomationBody.tsx index 8cc0c67ba40..b16223dccfd 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/components/AutomationBody/AutomationBody.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/components/AutomationBody/AutomationBody.tsx @@ -16,10 +16,18 @@ export function AutomationBody({ const [prompt, setPrompt] = useState(automation.prompt); const updateMutation = useMutation({ - mutationFn: (patch: { name?: string; prompt?: string }) => + mutationFn: (patch: { name?: string }) => apiTrpcClient.automation.update.mutate({ id: automation.id, ...patch }), }); + const setPromptMutation = useMutation({ + mutationFn: (next: string) => + apiTrpcClient.automation.setPrompt.mutate({ + id: automation.id, + prompt: next, + }), + }); + const hostTarget: WorkspaceHostTarget = automation.targetHostId ? { kind: "host", hostId: automation.targetHostId } : { kind: "local" }; @@ -47,7 +55,7 @@ export function AutomationBody({ onChange={setPrompt} onSave={(next) => { if (next !== automation.prompt) { - updateMutation.mutate({ prompt: next }); + setPromptMutation.mutate(next); } }} placeholder="Add prompt e.g. look for crashes in $sentry" diff --git a/apps/docs/content/docs/automations.mdx b/apps/docs/content/docs/automations.mdx index 56fabc8df76..35588423a0b 100644 --- a/apps/docs/content/docs/automations.mdx +++ b/apps/docs/content/docs/automations.mdx @@ -71,7 +71,9 @@ From the automation's detail page: ```bash superset automations list superset automations create --name "Nightly audit" --rrule "FREQ=DAILY;BYHOUR=3;BYMINUTE=0" --project --prompt-file prompt.md --agent claude +superset automations get superset automations run -superset automations logs +superset automations pause +superset automations resume superset automations delete ``` diff --git a/apps/docs/content/docs/cli/cli-reference.mdx b/apps/docs/content/docs/cli/cli-reference.mdx new file mode 100644 index 00000000000..c46d5347efc --- /dev/null +++ b/apps/docs/content/docs/cli/cli-reference.mdx @@ -0,0 +1,752 @@ +--- +title: CLI Reference +description: Complete reference for Superset command-line interface, including commands and flags. +--- + +## Synopsis + +```text +superset [subcommand] [options] +``` + +Run `superset --help` for the top-level command list, or +`superset --help` for any group. + +## Global options + +These flags are accepted by every command: + +| Flag | Env | Description | +| --- | --- | --- | +| `--json` | | Print the data payload as formatted JSON. | +| `--quiet` | | One ID per line for arrays; the ID for single objects; JSON fallback otherwise. | +| `--api-key ` | `SUPERSET_API_KEY` | Use an API key instead of stored OAuth login. | +| `--help`, `-h` | | Show help for the current command. | +| `--version`, `-v` | | Print `superset v` and exit. | + +--- + +## Commands + +### auth + +Authentication and session inspection. + +", + description: "Selects the active organization without prompting. Required for non-TTY logins when you belong to multiple orgs.", + }, + ]} +> +Authenticate via browser OAuth and store a session token at +`~/.superset/config.json`. + +Single-org accounts are selected automatically. With multiple orgs and a +TTY, the CLI prompts; with multiple orgs and no TTY, you must pass +`--organization` or the command exits 1 listing the available slugs. + +```bash +superset auth login +superset auth login --organization acme +``` + + +The newly authenticated user and active organization. + + +```text +◇ Authorized! +│ Satya Patel (you@example.com) +│ Organization: Acme +└ Logged in successfully. +``` + + + +```ts +{ + userId: string; + organizationId: string; + organizationName: string; +} +``` + + + + + +Clear the stored session. Does not call the API and does not clear the +active organization (your preferred org persists across re-logins). + +```bash +superset auth logout +``` + + +A confirmation message. + + +```text +Logged out. +``` + + + +```ts +{ message: "Logged out." } +``` + + + + + +Show the current user, active organization, and auth source. + +```bash +superset auth whoami +``` + + +The current user and organization context. + + +```text +Signed in as Satya Patel (you@example.com) +Organization: Acme +Auth: Session (expires in 32 min) +``` + + + +```ts +{ + userId: string; + email: string; + name: string; + organizationId: string; + organizationName: string; + authSource: "flag" | "env" | "oauth"; +} +``` + + + + +--- + +### start + +Start the local host service — the daemon the CLI and desktop app both +talk to when targeting this machine. + +", description: "Specific port. Default: a free loopback port." }, + ]} +> +If a manifest exists and the PID is alive, returns the existing +instance's details. Otherwise spawns the host service binary, polls +`/trpc/health.check` for up to 10 seconds, and writes the manifest. +Binds to `127.0.0.1` only. + +```bash +superset start --daemon +``` + + +The running host service. + + +```ts +{ + pid: number; + port: number; + organizationId: string; +} +``` + + + + +### stop + +Stop the local host service. + + +Sends SIGTERM, waits up to 10 seconds, sends SIGKILL if still alive. The +manifest is removed in all cases — including when SIGTERM itself throws. + +```bash +superset stop +``` + + +The stopped host service, if one was running. + + +```ts +// No manifest +{ running: false } + +// Manifest existed +{ pid: number; organizationId: string } +``` + + + + +### status + +Inspect the local host service. + + +`healthy` reflects a live `health.check` request (2-second timeout). + +```bash +superset status +``` + + +The host service state. Three shapes depending on what's running. + + +```ts +// No manifest +{ running: false; organizationId: string } + +// Manifest exists but PID is dead +{ + running: false; + stale: true; + pid: number; + organizationId: string; +} + +// Running +{ + running: true; + healthy: boolean; + pid: number; + port: number; + endpoint: string; + organizationId: string; + uptimeSec: number; +} +``` + + + + +### update + +Update the Superset CLI and host service binary. + +", description: "Install a specific version (e.g. `0.1.2`) instead of the rolling latest. Accepts upgrade or downgrade." }, + ]} +> +Downloads the matching `superset-.tar.gz` from GitHub Releases +and atomically replaces the install root. Only available in built +binaries; running from `bun run dev` errors out. + +```bash +superset update # upgrade to the latest release +superset update --check # show current → target without installing +superset update --version 0.1.2 # install a specific version (up or down) +``` + + +The current and target version, plus whether anything changed. + + +```text +Updated 0.1.4 → 0.1.5 (/Users/you/.local/share/superset) +``` + + + +```ts +// --check +{ + current: string; + target: string; + upToDate: boolean; + pinned: boolean; +} + +// install +{ + current: string; + target: string; + updated: boolean; + installRoot?: string; +} +``` + + + + +--- + +### organization (alias: `org`) + +Manage which organization the CLI targets. + +`} + humanColumns={["NAME", "SLUG", "ACTIVE"]} + quiet="organization IDs" +> +List organizations available to the current auth context. Marks the +active one. + + + +Set the active organization in `~/.superset/config.json`. + +```bash +superset organization switch acme +superset org switch org_… +``` + + +--- + +### projects + +Projects are checked-out repos that live on a specific host. v1 ships +read-only; create projects from the desktop app. + +", description: "Target host. Defaults to the local machine." }, + ]} + output={`Array<{ + id: string; + repoOwner: string; + repoName: string; + repoPath: string; +}>`} + humanColumns={["ID", "OWNER", "REPO", "PATH"]} + quiet="project IDs" +> +List projects on the target host. + + +--- + +### hosts + +Discover hosts registered to the active organization. To control the +host service running on this machine, use [`start`](#start), +[`stop`](#stop), and [`status`](#status). + +`} + humanColumns={["ID", "NAME", "ONLINE"]} + quiet="host IDs" +> +List hosts registered to the active organization. Host registration +happens via `superset start` on each machine — there is no +separate registration command. + +```bash +superset hosts list +``` + + +--- + +### workspaces (alias: `ws`) + +Workspaces are branch-scoped working copies on a host. + +**Routing:** when `--host` resolves to the local machine, the CLI calls +the host service directly over loopback HTTP — no cloud roundtrip, works +offline. Otherwise it routes through the cloud API and the relay. + +If the resolved host is the local machine but the host service isn't +responding, the CLI errors with +`Host service for this machine isn't running. Run: superset start.` +rather than silently falling through to the cloud. + +", description: "Target host. Defaults to the local machine." }, + ]} + output={`Array<{ + id: string; + name: string; + branch: string; + projectId: string; + projectName: string; + hostId: string; +}>`} + humanColumns={["ID", "NAME", "BRANCH", "PROJECT", "HOST"]} + quiet="workspace IDs" +> +List workspaces on the target host. + + +", required: true, description: "Project ID." }, + { flag: "--name ", required: true, description: "Workspace name." }, + { flag: "--branch ", required: true, description: "Workspace branch." }, + { flag: "--host ", description: "Defaults to the local machine." }, + ]} + output="Workspace" +> +Create a workspace on the target host. + +```bash +superset workspaces create \ + --project prj_… \ + --name "fix-login-bug" \ + --branch fix/login-bug +``` + + +", description: "Defaults to the local machine." }, + ]} + output={`{ deleted: string[] }`} +> +Delete one or more workspaces on the target host. + +```bash +superset workspaces delete ws_a ws_b ws_c +``` + + +--- + +### tasks (alias: `t`) + +Tasks are units of work in your organization's task tracker. + +", description: "Filter by status ID." }, + { flag: "--priority ", description: "Filter by priority." }, + { flag: "--assignee-me, -m", description: "Tasks assigned to the current user." }, + { flag: "--creator-me", description: "Tasks created by the current user." }, + { flag: "--search , -s", description: "Substring search on title." }, + { flag: "--limit ", description: "Default 50, max 200." }, + { flag: "--offset ", description: "Default 0." }, + ]} + output="Array" + humanColumns={["SLUG", "TITLE", "PRIORITY", "ASSIGNEE"]} + quiet="task IDs" +> +List tasks in the active organization. + +```bash +superset tasks list --assignee-me +superset t list -s "auth" --json +``` + + + +Get a task by ID or slug. + +```bash +superset tasks get tsk_… +superset tasks get fix-login-bug +``` + + +", required: true, description: "Task title." }, + { flag: "--description ", description: "Task description." }, + { flag: "--priority ", description: "Priority." }, + { flag: "--assignee ", description: "Assignee user ID." }, + ]} + output="Task" +> +Create a task. + +```bash +superset tasks create --title "Audit auth flow" --priority high +``` + + +", description: "New title." }, + { flag: "--description ", description: "New description." }, + { flag: "--priority ", description: "New priority." }, + { flag: "--assignee ", description: "New assignee." }, + ]} + output="Task" +> +Update a task. Same fields as `create`, all optional. + +```bash +superset tasks update fix-login-bug --priority urgent +``` + + + +Delete one or more tasks. + +```bash +superset tasks delete tsk_a tsk_b +``` + + +--- + +### automations (alias: `auto`) + +Scheduled agent runs. Schedules use RFC 5545 RRULE bodies, e.g. +`FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0`. + + +List automations in the active organization. + + + +Get an automation's metadata. The prompt body is omitted — use +[`automations prompt`](#superset-automations-prompt-id) to read it. +Use [`automations logs`](#superset-automations-logs-id) for run history. + + +", required: true, description: "Automation name." }, + { flag: "--prompt ", description: "Inline prompt; pass `-` to read stdin." }, + { flag: "--prompt-file ", description: "Read prompt from file, verbatim." }, + { flag: "--rrule ", required: true, description: "RFC 5545 RRULE schedule." }, + { flag: "--timezone ", description: "Default: host TZ, then UTC." }, + { flag: "--dtstart ", description: "Default: now." }, + { flag: "--workspace ", description: "Reuse an existing workspace." }, + { flag: "--project ", required: true, description: "Project ID." }, + { flag: "--host ", description: "Default: owner's online host." }, + { flag: "--agent ", description: "Default: claude." }, + { flag: "--agent-config-file ", description: "Full ResolvedAgentConfig JSON; overrides --agent." }, + ]} + output="Automation" +> +Create an automation. Exactly one of `--prompt` or `--prompt-file` must +be provided. + +```bash +superset automations create \ + --name "Weekday triage" \ + --project prj_… \ + --workspace ws_… \ + --rrule "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0" \ + --prompt-file ./prompts/triage.md +``` + + +", description: "New name." }, + { flag: "--rrule ", description: "New schedule." }, + { flag: "--timezone ", description: "New timezone." }, + { flag: "--dtstart ", description: "New start time." }, + { flag: "--host ", description: "New target host." }, + { flag: "--agent ", description: "New agent preset." }, + { flag: "--agent-config-file ", description: "Overrides --agent when both provided." }, + { flag: "--enabled / --no-enabled", description: "Calls automation.setEnabled first." }, + ]} + output="Automation" +> +Update an automation's metadata (name, schedule, agent, host). All flags +optional. Omitting a flag preserves the existing value — `undefined` means +"no change", not "clear". Use [`automations prompt`](#superset-automations-prompt-id) +to read or replace the prompt body. + + +", description: "Read the new prompt from a file. Use `-` for stdin." }, + ]} + output="Markdown (read mode) or Automation (write mode)" +> +Read or replace an automation's prompt body. Without `--from-file`, prints +the current prompt to stdout. With `--from-file ` or piped stdin, +replaces the prompt verbatim. + +```bash +# Read +superset automations prompt aut_… > prompt.md + +# Write from file +superset automations prompt aut_… --from-file ./prompt.md + +# Write from stdin +cat ./prompt.md | superset automations prompt aut_… +``` + + + +Delete an automation. + +```bash +superset automations delete aut_… +``` + + + +Pause an automation (sets `enabled: false`). + +```bash +superset automations pause aut_… +``` + + + +Resume an automation (sets `enabled: true`). The API recomputes +`nextRunAt` on resume. + +```bash +superset automations resume aut_… +``` + + + +Dispatch an automation immediately. Does not wait for completion. + +```bash +superset automations run aut_… +``` + + +", description: "Default 20, max 100." }, + ]} + output="Array" + humanColumns={["ID", "STATUS", "SCHEDULED FOR", "CREATED AT", "ERROR"]} + quiet="run IDs" +> +List recent runs for an automation. + +```bash +superset automations logs aut_… +``` + + +--- + +## Output modes + +**JSON mode** (`--json`): raw payloads. Lists print arrays, get/create/update +print objects, delete prints summary objects. No `{ "data": ... }` wrapper. +Empty results print `null`. + +**Quiet mode** (`--quiet`): IDs only. Arrays of objects with an `id` field +print one ID per line; single objects print their `id`; everything else +falls back to JSON. + +When `CLAUDE_CODE`, `CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT`, `CODEX_CLI`, +`GEMINI_CLI`, `SUPERSET_AGENT`, or `CI` is set to a non-empty value, output +defaults to JSON unless `--quiet` is provided. diff --git a/apps/docs/content/docs/cli/env-vars.mdx b/apps/docs/content/docs/cli/env-vars.mdx new file mode 100644 index 00000000000..ee0dc8b5817 --- /dev/null +++ b/apps/docs/content/docs/cli/env-vars.mdx @@ -0,0 +1,14 @@ +--- +title: Environment variables +description: Complete reference for environment variables that control Superset CLI behavior. +--- + +The Superset CLI supports the following environment variables to control +its behavior. Set them in your shell before running `superset`, or +export them from your shell profile (`~/.zshrc`, `~/.bashrc`) or your +CI environment to apply them across every session. + +| Variable | Description | +| --- | --- | +| `SUPERSET_API_KEY` | API key (`sk_live_…` / `sk_test_…`) used in place of stored OAuth login. Equivalent to `--api-key`. | +| `SUPERSET_HOME_DIR` | Directory the CLI uses for `config.json` and the host service tree. Default: `~/.superset`. Shared with the desktop app. | diff --git a/apps/docs/content/docs/cli/getting-started.mdx b/apps/docs/content/docs/cli/getting-started.mdx new file mode 100644 index 00000000000..230fbee1c51 --- /dev/null +++ b/apps/docs/content/docs/cli/getting-started.mdx @@ -0,0 +1,170 @@ +--- +title: Getting Started +description: Install the Superset CLI, sign in, and run your first commands. +--- + +The Superset CLI (`superset`) is a single static binary that drives the same +backend as the desktop app. Use it to manage workspaces, tasks, automations, +and the local host service from your terminal or from CI. + +## Install + +### macOS (Apple Silicon) + +```bash +curl -fsSL https://app.superset.sh/install.sh | sh +``` + +The script downloads the latest `superset-darwin-arm64` binary and installs +it at `/usr/local/bin/superset`. + +### Linux (x86_64) + +```bash +curl -fsSL https://app.superset.sh/install.sh | sh +``` + +### From source + +```bash +git clone https://github.com/superset-sh/superset.git +cd superset +bun install +bun --filter @superset/cli build +``` + +The built binary lands at `packages/cli/dist/superset`. + +## Sign in + +```bash +superset auth login +``` + +The CLI starts a loopback callback server on `127.0.0.1:51789` (or `51790` +if busy), opens your browser to the consent page, and stores a session +token at `~/.superset/config.json`. + +If you belong to multiple organizations and stdout is a TTY, you'll be +prompted to pick one. In CI or any non-TTY environment, pass it explicitly: + +```bash +superset auth login --organization acme +``` + +To use an API key instead of OAuth (recommended for CI): + +```bash +export SUPERSET_API_KEY=sk_live_… +superset auth whoami +``` + +API keys are issued from the **Settings → API Keys** page in the web app. + +## Verify your session + +```bash +superset auth whoami +``` + +```text +Signed in as Satya Patel (you@example.com) +Organization: Acme +Auth: Session (expires in 32 min) +``` + +## Start the local host service + +The host service is the daemon that the CLI and desktop app both talk to +when targeting the local machine. It manages workspaces, ports, and agent +runs. The desktop app starts it automatically; from the CLI: + +```bash +superset start --daemon +``` + +```bash +superset status +``` + +Stop it again with `superset stop`. State is shared with the desktop +app under `~/.superset/host//`, so a host service started +by either client is visible to both. + +## Run your first commands + +List workspaces on the local machine: + +```bash +superset workspaces list +``` + +Create one against an existing project: + +```bash +superset projects list +superset workspaces create \ + --project prj_… \ + --name "fix-login-bug" \ + --branch fix/login-bug +``` + +Trigger an automation immediately: + +```bash +superset automations list +superset automations run aut_… +superset automations logs aut_… --follow +``` + +## Output modes + +Every command prints a TTY-friendly view by default. Two flags switch the +shape of the output for scripting: + +| Flag | Output | +| --- | --- | +| `--json` | The data payload as formatted JSON. No envelope. | +| `--quiet` | One ID per line for arrays of objects with `id`; the ID for single objects; JSON otherwise. | + +```bash +superset tasks list --status in_progress --json | jq '.[].title' +superset workspaces list --quiet | xargs -L1 superset workspaces delete +``` + +When the CLI detects an agent or CI environment (`CLAUDE_CODE`, +`CLAUDECODE`, `CLAUDE_CODE_ENTRYPOINT`, `CODEX_CLI`, `GEMINI_CLI`, +`SUPERSET_AGENT`, or `CI` set to a non-empty value), it defaults to JSON +output unless you pass `--quiet`. + +## Local vs. remote hosts + +`workspaces`, `projects`, and `automations create/update` accept a +`--host ` flag to pick which host services the request. When omitted, +the CLI targets the local machine, talks directly to the host service over +loopback, and works offline. + +To target a different host: + +```bash +superset hosts list +superset workspaces list --host h_… +``` + +Remote calls go through the cloud API and the relay; behavior is otherwise +identical. + +## Where state lives + +```text +~/.superset/config.json # auth, active organization +~/.superset/host//manifest.json # host service endpoint + token +~/.superset/host//host.db # local host service DB +``` + +`SUPERSET_HOME_DIR` relocates the whole tree, matching the desktop app. + +## Next steps + +Continue to the [CLI reference](/cli/cli-reference) for every command, +flag, and output shape. diff --git a/apps/docs/content/docs/cli/meta.json b/apps/docs/content/docs/cli/meta.json new file mode 100644 index 00000000000..5874a81a249 --- /dev/null +++ b/apps/docs/content/docs/cli/meta.json @@ -0,0 +1,13 @@ +{ + "title": "CLI", + "description": "Command line interface", + "icon": "Terminal", + "root": true, + "pages": [ + "---Rocket Get Started---", + "getting-started", + "---BookOpen Reference---", + "cli-reference", + "env-vars" + ] +} diff --git a/apps/docs/src/app/(docs)/[[...slug]]/components/DocsPageLayout/index.ts b/apps/docs/src/app/(docs)/[[...slug]]/components/DocsPageLayout/index.ts index 8514b5af002..60b452e805a 100644 --- a/apps/docs/src/app/(docs)/[[...slug]]/components/DocsPageLayout/index.ts +++ b/apps/docs/src/app/(docs)/[[...slug]]/components/DocsPageLayout/index.ts @@ -1 +1,6 @@ -export { DocsBody, DocsPage, DocsTitle } from "./DocsPageLayout"; +export { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, +} from "./DocsPageLayout"; diff --git a/apps/docs/src/app/(docs)/[[...slug]]/page.tsx b/apps/docs/src/app/(docs)/[[...slug]]/page.tsx index e25fc1f7db3..09570bc936a 100644 --- a/apps/docs/src/app/(docs)/[[...slug]]/page.tsx +++ b/apps/docs/src/app/(docs)/[[...slug]]/page.tsx @@ -2,7 +2,12 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { getPageImage, source } from "@/lib/source"; import { getMDXComponents } from "@/mdx-components"; -import { DocsBody, DocsPage, DocsTitle } from "./components/DocsPageLayout"; +import { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, +} from "./components/DocsPageLayout"; import { LLMCopyButton, ViewOptions } from "./components/PageActions"; export default async function Page(props: PageProps<"/[[...slug]]">) { @@ -26,6 +31,7 @@ export default async function Page(props: PageProps<"/[[...slug]]">) { }} > {page.data.title} + {page.data.description}
getActiveProductId(pathname), + [pathname], + ); + const activeProduct = useMemo( + () => products.find((p) => p.id === activeProductId) ?? products[0], + [activeProductId], + ); + const sections = activeProduct?.sections ?? []; + + const [productMenuOpen, setProductMenuOpen] = useState(false); const [openSections, setOpenSections] = useState(() => Array.from({ length: sections.length }, (_, i) => i), ); const { setOpenSearch } = useSearchContext(); - const pathname = usePathname(); + + useEffect(() => { + setOpenSections(Array.from({ length: sections.length }, (_, i) => i)); + }, [sections.length]); useEffect(() => { const currentSection = sections.findIndex((section) => section.items.some((item) => item.href === pathname), ); - if (currentSection !== -1 && !openSections.includes(currentSection)) { - setOpenSections((prev) => [...prev, currentSection]); + if (currentSection !== -1) { + setOpenSections((prev) => + prev.includes(currentSection) ? prev : [...prev, currentSection], + ); } - }, [pathname, openSections]); + }, [pathname, sections]); + + useEffect(() => { + if (!productMenuOpen) return; + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") setProductMenuOpen(false); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [productMenuOpen]); const toggleSection = (index: number) => { setOpenSections((prev) => @@ -32,6 +59,10 @@ export default function Sidebar() { ); }; + if (!activeProduct) return null; + + const ActiveIcon = activeProduct.Icon; + return (