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' },