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
4 changes: 2 additions & 2 deletions apps/docs/content/docs/cli/files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ sim files versions list <fileId> [options]

</CommandTable>

## Read the text content of one version of a file
## Read the text content of one version of a file as JSON or YAML

```bash
sim files versions read <fileId> <version> [options]
Expand Down Expand Up @@ -474,7 +474,7 @@ Also available as `sim files mv`.

</CommandTable>

## Read a file’s text content
## Read a file’s text content as JSON or YAML

```bash
sim files read <fileId> [options]
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ sim files versions list <fileId> [options]

### sim files versions read

Read the text content of one version of a file
Read the text content of one version of a file as JSON or YAML

```bash
sim files versions read <fileId> <version> [options]
Expand Down Expand Up @@ -1258,7 +1258,7 @@ Also available as `sim files mv`.

### sim files read

Read a file’s text content
Read a file’s text content as JSON or YAML

```bash
sim files read <fileId> [options]
Expand Down Expand Up @@ -6293,7 +6293,7 @@ sim workflows create [options]
| --- | --- | --- |
| `--name <value>` | Yes | Workflow name. |
| `--description <value>` | No | Optional workflow description. |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
| `--folder <value>` | No | Existing folder path (leading / optional); create it first with sim workflows mkdir &lt;path&gt;. |

</CommandTable>

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ sim workflows create [options]
| --- | --- | --- |
| `--name <value>` | Yes | Workflow name. |
| `--description <value>` | No | Optional workflow description. |
| `--folder <value>` | No | Folder path as shown in the app; the leading / is optional. |
| `--folder <value>` | No | Existing folder path (leading / optional); create it first with sim workflows mkdir &lt;path&gt;. |

</CommandTable>

Expand Down
10 changes: 10 additions & 0 deletions packages/sim-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ For each setting, the CLI uses the first available value in this order:
4. built-in default

`sim whoami` shows both the resolved values and where each one came from.
Its JSON and YAML `authenticated` field is `true` after the server accepts the
credential, `false` when it is missing or rejected, and `null` when authentication
could not be checked (including `--no-verify`). `verification.status` separately
reports whether the configured workspace is accessible; a valid credential can
still have `no-workspace` or `rejected` workspace verification.

`sim files read` and `sim files versions read` print the complete text response as
JSON, or YAML with `--output yaml`, including in table and text display modes.
The `truncated` field describes server extraction limits, not display clipping.
For the original file bytes, use `sim files get`.

## Useful commands

Expand Down
144 changes: 144 additions & 0 deletions packages/sim-cli/src/commands/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,150 @@ describe('whoami command', () => {
expect(output).not.toContain('secret')
})

it.each([
{ workspaceId: null, status: 401 },
{ workspaceId: null, status: 403 },
{ workspaceId: 'ws_1', status: 401 },
{ workspaceId: 'ws_1', status: 403 },
])(
'reports credential rejection with workspace=$workspaceId and HTTP $status',
async ({ workspaceId, status }) => {
mocks.profileFrom.mockReturnValue(configured({ workspaceId, output: 'json' }))
mocks.request.mockRejectedValue(new SimApiError('Credential rejected', status))

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBe(false)
expect(result.verification.status).toBe('rejected')
expect(result.verification.detail).toBe('Credential rejected')
expect(process.exitCode).toBe(1)
}
)

it.each([0, 404, 429, 502])(
'preserves setup guidance without claiming authentication when metadata is unavailable with HTTP %s',
async (status) => {
mocks.profileFrom.mockReturnValue(configured({ workspaceId: null, output: 'json' }))
mocks.request.mockRejectedValue(new SimApiError('Metadata unavailable', status))

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBeNull()
expect(result.verification.status).toBe('no-workspace')
expect(result.verification.detail).toContain(
'sim configure --profile default --set-workspace'
)
expect(process.exitCode).toBe(2)
}
)

it('distinguishes accepted credentials from missing workspace configuration', async () => {
mocks.profileFrom.mockReturnValue(configured({ workspaceId: null, output: 'json' }))

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBe(true)
expect(result.verification.status).toBe('no-workspace')
expect(process.exitCode).toBe(2)
})

it('keeps authentication separate from workspace access', async () => {
mocks.profileFrom.mockReturnValue(configured({ output: 'json' }))
mocks.request.mockImplementation(async (path: string) => {
if (path === '/api/v2/meta') return { data: { keyType: 'workspace' } }
throw new SimApiError('Workspace not found', 404)
})

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBe(true)
expect(result.verification.status).toBe('rejected')
expect(process.exitCode).toBe(1)
})

it('leaves authentication unknown when verification is explicitly skipped', async () => {
mocks.profileFrom.mockReturnValue(configured({ output: 'json' }))
mocks.request.mockImplementation(async () => {
throw new Error('Verification must be skipped')
})

await whoami('--no-verify')

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBeNull()
expect(result.verification.status).toBe('disabled')
expect(process.exitCode).toBeUndefined()
})

it('still verifies a workspace when an older server has no metadata endpoint', async () => {
mocks.profileFrom.mockReturnValue(configured({ output: 'json' }))
mocks.request.mockImplementation(async (path: string) => {
if (path === '/api/v2/meta') throw new SimApiError('Not found', 404)
return { data: { id: 'ws_1', name: 'Workspace', memberCount: 1 } }
})

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBe(true)
expect(result.verification.status).toBe('verified')
expect(process.exitCode).toBeUndefined()
})

it.each([
undefined,
null,
{},
{ data: null },
{ data: {} },
{ data: { keyType: 'unsupported' } },
])('falls back to workspace verification for malformed metadata %j', async (meta) => {
mocks.profileFrom.mockReturnValue(configured({ output: 'json' }))
mocks.request.mockImplementation(async (path: string) =>
path === '/api/v2/meta' ? meta : { data: { id: 'ws_1', name: 'Workspace', memberCount: 1 } }
)

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBe(true)
expect(result.verification.status).toBe('verified')
expect(result.verification.keyType).toBeNull()
expect(process.exitCode).toBeUndefined()
})

it('keeps authentication unknown when malformed metadata is the only possible check', async () => {
mocks.profileFrom.mockReturnValue(configured({ workspaceId: null, output: 'json' }))
respond({ data: { keyType: 'unsupported' } })

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBeNull()
expect(result.verification.status).toBe('no-workspace')
expect(result.verification.keyType).toBeNull()
expect(process.exitCode).toBe(2)
})

it('reports a key revoked between the metadata and workspace reads as unauthenticated', async () => {
mocks.profileFrom.mockReturnValue(configured({ output: 'json' }))
mocks.request.mockImplementation(async (path: string) => {
if (path === '/api/v2/meta') return { data: { keyType: 'personal' } }
throw new SimApiError('Invalid API key', 401)
})

await whoami()

const result = JSON.parse(vi.mocked(console.log).mock.calls.flat().join('\n'))
expect(result.authenticated).toBe(false)
expect(result.verification.status).toBe('rejected')
expect(process.exitCode).toBe(1)
})

it('exits 1 when the API rejects the key, without hiding the resolved settings', async () => {
mocks.request.mockRejectedValue(
new SimApiError('Invalid API key — run: sim login --profile default', 401)
Expand Down
Loading
Loading