From cbdd60fd15846a3550c4a7483989cddff7720ab5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 17 Aug 2026 16:14:34 -0700 Subject: [PATCH] fix(cli): default to the host that serves the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sim.ai` answers /api/** with a 301 to `www.sim.ai`, and the client refuses to follow redirects — a 301 rewrites a POST into a bodyless GET, so following one turns a write into a silent no-op and hands the API key to whatever host Location names. Defaulting to the apex therefore failed every command for anyone who never set an endpoint. Before the refusal shipped it was quieter and worse: reads succeeded while writes did nothing. Also trims the provider catalogue from eleven inferred columns to seven. `docsUrl`, `helpText`, `requiresClientGeneratedCredentialId` and the nested `fields` are what you read once you have chosen a provider, not what you scan to choose one, and they pushed the table well past a terminal. Both ids stay: `credentials connect` names an OAuth provider by `serviceId`, `credentials create` matches a service account on `providerId`. --- .../content/docs/en/cli/authentication.mdx | 2 +- .../content/docs/en/cli/configuration.mdx | 4 ++-- .../content/docs/en/cli/troubleshooting.mdx | 2 +- packages/sim-cli/README.md | 4 ++-- .../sim-cli/src/commands/configure.test.ts | 4 ++-- packages/sim-cli/src/config/profile.test.ts | 17 +++++++++++++--- packages/sim-cli/src/config/profile.ts | 13 +++++++++++- .../sim-cli/src/contract/commands.test.ts | 18 +++++++++++++++++ packages/sim-cli/src/contract/commands.ts | 20 +++++++++++++++++++ 9 files changed, 72 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/docs/en/cli/authentication.mdx b/apps/docs/content/docs/en/cli/authentication.mdx index 72ad91065b0..617015ef35c 100644 --- a/apps/docs/content/docs/en/cli/authentication.mdx +++ b/apps/docs/content/docs/en/cli/authentication.mdx @@ -20,7 +20,7 @@ The terminal prints a pairing code and a URL: Pairing code: K7M2-P9XT Confirm this code matches what the browser shows before approving. -https://sim.ai/cli/auth?request=…&scope=platform +https://www.sim.ai/cli/auth?request=…&scope=platform Waiting for approval… ✓ Logged in. Key stored in /Users/you/.sim/credentials diff --git a/apps/docs/content/docs/en/cli/configuration.mdx b/apps/docs/content/docs/en/cli/configuration.mdx index ed62657c6d8..564bc07cef6 100644 --- a/apps/docs/content/docs/en/cli/configuration.mdx +++ b/apps/docs/content/docs/en/cli/configuration.mdx @@ -52,7 +52,7 @@ Each setting resolves independently, and the first match wins: | 1 | Command-line flag — `--endpoint`, `--workspace`, `--output` | | 2 | Environment — `SIM_ENDPOINT`, `SIM_API_KEY`, `SIM_WORKSPACE`, `SIM_OUTPUT` | | 3 | `~/.sim/config` and `~/.sim/credentials`, for the selected profile | -| 4 | Built-in default — `https://sim.ai` and `table` | +| 4 | Built-in default — `https://www.sim.ai` and `table` | `sim whoami` prints the winning source for each setting: @@ -67,7 +67,7 @@ repo: ```ini title="~/.sim/config" [default] -endpoint = https://sim.ai +endpoint = https://www.sim.ai workspace = ws_abc123 output = table diff --git a/apps/docs/content/docs/en/cli/troubleshooting.mdx b/apps/docs/content/docs/en/cli/troubleshooting.mdx index 4d3e2c59c73..c8ecdcef039 100644 --- a/apps/docs/content/docs/en/cli/troubleshooting.mdx +++ b/apps/docs/content/docs/en/cli/troubleshooting.mdx @@ -37,7 +37,7 @@ re-authenticating: sim whoami --profile ``` -## `Could not reach https://sim.ai: ` +## `Could not reach https://www.sim.ai: ` The request never got a response: DNS, TLS, a proxy, or a self-hosted stack that is not running. Confirm the endpoint the CLI actually used with `sim whoami`, and diff --git a/packages/sim-cli/README.md b/packages/sim-cli/README.md index dbbbbd1c9d8..f6c44366817 100644 --- a/packages/sim-cli/README.md +++ b/packages/sim-cli/README.md @@ -20,7 +20,7 @@ Non-secret settings live in `~/.sim/config`: ```ini [default] -endpoint = https://sim.ai +endpoint = https://www.sim.ai workspace = ws_abc123 output = table @@ -58,7 +58,7 @@ Each setting resolves independently, first match wins: | 1 | Command-line flag (`--endpoint`, `--workspace`, `--output`) | | 2 | Environment (`SIM_ENDPOINT`, `SIM_API_KEY`, `SIM_WORKSPACE`, `SIM_OUTPUT`) | | 3 | `~/.sim/config` / `~/.sim/credentials` for the selected profile | -| 4 | Built-in default (`https://sim.ai`, `table`) | +| 4 | Built-in default (`https://www.sim.ai`, `table`) | Formats are listed under [Output formats](#output-formats). diff --git a/packages/sim-cli/src/commands/configure.test.ts b/packages/sim-cli/src/commands/configure.test.ts index 4a4bbb64080..d06a956d541 100644 --- a/packages/sim-cli/src/commands/configure.test.ts +++ b/packages/sim-cli/src/commands/configure.test.ts @@ -33,14 +33,14 @@ afterEach(() => { describe('configure --set-endpoint', () => { it('refuses to store an endpoint that would later crash the URL parser', async () => { await expect(run('--set-endpoint', 'not-a-url')).rejects.toThrow( - 'Invalid endpoint "not-a-url" from --set-endpoint. Use an absolute URL, e.g. https://sim.ai or http://localhost:3000' + 'Invalid endpoint "not-a-url" from --set-endpoint. Use an absolute URL, e.g. https://www.sim.ai or http://localhost:3000' ) expect(readConfigProfile('default')).toEqual({}) }) it('refuses a scheme the HTTP client cannot speak', async () => { await expect(run('--set-endpoint', 'ftp://x.com')).rejects.toThrow( - 'Unsupported endpoint scheme "ftp" from --set-endpoint. Use http or https, e.g. https://sim.ai' + 'Unsupported endpoint scheme "ftp" from --set-endpoint. Use http or https, e.g. https://www.sim.ai' ) expect(readConfigProfile('default')).toEqual({}) }) diff --git a/packages/sim-cli/src/config/profile.test.ts b/packages/sim-cli/src/config/profile.test.ts index 0e9d13d3d87..1d457be9b2a 100644 --- a/packages/sim-cli/src/config/profile.test.ts +++ b/packages/sim-cli/src/config/profile.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { configPath, credentialsPath } from './paths' import { + DEFAULT_ENDPOINT, deleteProfile, listProfiles, OUTPUT_FORMATS, @@ -31,7 +32,7 @@ describe('profile resolution', () => { it('falls back to built-in defaults with nothing configured', () => { const profile = resolveProfile() expect(profile.name).toBe('default') - expect(profile.endpoint).toBe('https://sim.ai') + expect(profile.endpoint).toBe('https://www.sim.ai') expect(profile.apiKey).toBeNull() expect(profile.output).toBe('table') expect(profile.sources.apiKey).toBe('unset') @@ -92,13 +93,23 @@ describe('profile resolution', () => { expect(resolveProfile({ profile: 'default' }).name).toBe('default') }) + it('defaults to the host that serves the API, not the apex that redirects to it', () => { + // `sim.ai` answers /api/** with a 301 to `www.sim.ai`, and the client + // refuses redirects because following one rewrites a POST into a bodyless + // GET. Defaulting to the apex therefore broke every command for anyone who + // never set an endpoint, so the host itself is the assertion. + expect(DEFAULT_ENDPOINT).toBe('https://www.sim.ai') + expect(new URL(DEFAULT_ENDPOINT).hostname).toBe('www.sim.ai') + expect(resolveProfile().endpoint).toBe(DEFAULT_ENDPOINT) + }) + it('strips a trailing slash so paths do not double up', () => { expect(resolveProfile({ endpoint: 'https://sim.ai///' }).endpoint).toBe('https://sim.ai') }) it('fails fast on an endpoint Node cannot parse, naming the source', () => { expect(() => resolveProfile({ endpoint: 'not-a-url' })).toThrow( - 'Invalid endpoint "not-a-url" from flag. Use an absolute URL, e.g. https://sim.ai or http://localhost:3000' + 'Invalid endpoint "not-a-url" from flag. Use an absolute URL, e.g. https://www.sim.ai or http://localhost:3000' ) process.env.SIM_ENDPOINT = 'not-a-url' @@ -111,7 +122,7 @@ describe('profile resolution', () => { it('rejects a parseable endpoint the HTTP client could never call', () => { expect(() => resolveProfile({ endpoint: 'ftp://x.com' })).toThrow( - 'Unsupported endpoint scheme "ftp" from flag. Use http or https, e.g. https://sim.ai' + 'Unsupported endpoint scheme "ftp" from flag. Use http or https, e.g. https://www.sim.ai' ) }) diff --git a/packages/sim-cli/src/config/profile.ts b/packages/sim-cli/src/config/profile.ts index 8de5402d580..9a91e704401 100644 --- a/packages/sim-cli/src/config/profile.ts +++ b/packages/sim-cli/src/config/profile.ts @@ -12,7 +12,18 @@ import { import { configPath, credentialsPath } from './paths' export const DEFAULT_PROFILE = 'default' -export const DEFAULT_ENDPOINT = 'https://sim.ai' + +/** + * The API host, which is the `www` one and not the apex. + * + * `sim.ai` answers `/api/**` with a 301 to `www.sim.ai`, and the CLI refuses to + * follow a redirect — a 301 rewrites a POST into a bodyless GET, so following + * one turns a write into a silent no-op and hands the API key to whatever host + * `Location` names. Defaulting to the apex therefore made every command fail + * for anyone who never set an endpoint, and before the refusal existed it was + * worse: reads succeeded while writes quietly did nothing. + */ +export const DEFAULT_ENDPOINT = 'https://www.sim.ai' /** * Output formats, in the order `--help` lists them. diff --git a/packages/sim-cli/src/contract/commands.test.ts b/packages/sim-cli/src/contract/commands.test.ts index c537ecbaee3..8be7fa00cf6 100644 --- a/packages/sim-cli/src/contract/commands.test.ts +++ b/packages/sim-cli/src/contract/commands.test.ts @@ -175,4 +175,22 @@ describe('folder-path fields', () => { } expect(undecoded).toEqual([]) }) + + it('keeps the provider catalogue to what you scan to choose one', () => { + // Inferred, this listed eleven columns: the detail-view fields + // (`docsUrl`, `helpText`, `requiresClientGeneratedCredentialId`) pushed the + // table well past a terminal and read as empty on every OAuth row. + const columns = CLI_CONTRACT.listCredentialProviders?.columns ?? [] + const paths = columns.map((column) => column.path ?? column.header) + + expect(columns.length).toBeLessThanOrEqual(7) + for (const detail of ['docsUrl', 'helpText', 'requiresClientGeneratedCredentialId', 'fields']) { + expect(paths).not.toContain(detail) + } + // Both ids stay: `credentials connect` names an OAuth provider by + // `serviceId`, `credentials create` matches a service account on + // `providerId`, and the catalogue is where you look either up. + expect(paths).toContain('serviceId') + expect(paths).toContain('providerId') + }) }) diff --git a/packages/sim-cli/src/contract/commands.ts b/packages/sim-cli/src/contract/commands.ts index 8e522b3f37a..66622e9d38a 100644 --- a/packages/sim-cli/src/contract/commands.ts +++ b/packages/sim-cli/src/contract/commands.ts @@ -435,6 +435,26 @@ export const CLI_CONTRACT: CliContract = { { header: 'updated', path: 'updatedAt', format: 'timestamp' }, ], }, + // Inferred, this was eleven columns wide, four of them belonging to a detail + // view rather than a catalogue: `docsUrl`, `helpText`, + // `requiresClientGeneratedCredentialId` and the nested `fields` are what you + // read once you have chosen a provider, not what you scan to choose one. + // + // Both ids stay, because the next command takes one or the other and which + // depends on the row: `credentials connect` names an OAuth provider by + // `serviceId`, while `credentials create` matches a service-account provider + // on `providerId`. Each is empty on the kind of row that does not use it. + listCredentialProviders: { + columns: [ + { header: 'type' }, + { header: 'service', path: 'serviceId' }, + { header: 'provider', path: 'providerId' }, + { header: 'name' }, + { header: 'family', path: 'providerFamily' }, + { header: 'available', format: 'bool' }, + { header: 'description' }, + ], + }, listSecrets: { columns: [ { header: 'name' },