Skip to content

Commit 7da8bcc

Browse files
committed
Merge remote-tracking branch 'origin/staging' into codex/integration-actions-simplify
2 parents 0a437dd + d7fb4f7 commit 7da8bcc

51 files changed

Lines changed: 2684 additions & 326 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/cli/commands.mdx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ sim configure [options]
124124

125125
</CommandTable>
126126

127+
## Update this global CLI installation to the newest release on its channel
128+
129+
```bash
130+
sim update [options]
131+
```
132+
133+
**Options**
134+
135+
<CommandTable>
136+
137+
| Option | Required | Description |
138+
| --- | --- | --- |
139+
| `--package-manager <manager>` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |
140+
141+
</CommandTable>
142+
127143
## Ask Sim and print the reply
128144

129145
```bash

‎apps/docs/content/docs/cli/configuration.mdx‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,27 @@ endpoint or stored login.
120120
| `SIM_CREDENTIALS_FILE` | Relocate only the credentials file |
121121
| `SIM_TIMEOUT_SECONDS` | Per-request timeout; `0` waits indefinitely. Defaults to `3600`, above every timeout the server itself applies |
122122
| `SIM_DEBUG` | Trace each request's method, URL, status and duration to stderr |
123-
| `SIM_NO_UPDATE_CHECK` | Turn off update checks |
123+
| `SIM_NO_UPDATE_CHECK` | Turn off update checks and notices |
124124

125-
## Update notices
125+
## Updates
126126

127-
The CLI checks for a newer release at most once per day on eligible interactive
128-
invocations. Notices go to stderr and show an upgrade command for the package
129-
manager that installed Sim.
127+
The CLI checks for a newer stable release at most once per day on eligible
128+
interactive invocations. It prints an optional notice to stderr and continues
129+
your command. Installation only happens when you run `sim update`.
130+
131+
Run `sim update` to update immediately, including in CI and when automatic checks
132+
are disabled. It requires a global installation and verifies that the package
133+
manager targets the running copy before installing. Supported managers are npm,
134+
pnpm, Bun, and Yarn Classic. For custom installations, select the manager with
135+
`sim update --package-manager bun`. Project-local and temporary package-runner
136+
copies must be updated through their package manager.
137+
138+
Manual updates preserve the stable, staging, or dev release channel. Installation
139+
failures stop with an error; concurrent update attempts are refused. Installer
140+
output goes to stderr and does not mix with JSON output on stdout.
141+
The updater resolves the channel through the selected package manager before
142+
installing. Older registry or mirror releases are refused; a newer release is
143+
installed by its exact version so a moving tag cannot change the target.
130144

131145
Checks are skipped in CI, when stderr is redirected, under `npm exec` or `npx`,
132146
from a repository checkout, and for prerelease versions. Set
@@ -141,7 +155,9 @@ malformed non-empty values disable the check. Redirects are not followed.
141155
The daily cache is `~/.sim/update-check.json`, or under `SIM_CONFIG_DIR`.
142156
`SIM_CONFIG_FILE` and `SIM_CREDENTIALS_FILE` do not relocate it. If the cache
143157
cannot be written, eligible invocations may check again. Concurrent commands
144-
can also each check. Requests have a one-second deadline.
158+
can also each check. Registry checks have a one-second deadline; package-manager
159+
installation has a five-minute deadline. Registry-check failures suppress the
160+
check, while installer failures are reported.
145161

146162
Node's `fetch` uses `HTTP(S)_PROXY` when opted in with `NODE_USE_ENV_PROXY=1`
147163
(Node 22.21+ or 24.0+) or `--use-env-proxy` (Node 22.21+ or 24.5+).

‎apps/docs/content/docs/cli/index.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ or local configuration.
3434

3535
Requires Node.js 20 or newer. Verify with `sim --version`.
3636

37+
The CLI shows optional update notices on eligible interactive invocations. Run
38+
`sim update` when you want to install the update. See
39+
[Updates](/cli/configuration#updates) for installation requirements and how to
40+
disable notices.
41+
3742
To run it without installing, use `npx sim <command>`.
3843

3944
Using Sim as a library instead? See the [TypeScript](/api-reference/typescript)

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ sim configure [options]
103103

104104
</CommandTable>
105105

106+
## sim update
107+
108+
Update this global CLI installation to the newest release on its channel
109+
110+
```bash
111+
sim update [options]
112+
```
113+
114+
**Options**
115+
116+
<CommandTable>
117+
118+
| Option | Required | Description |
119+
| --- | --- | --- |
120+
| `--package-manager <manager>` | No | Package manager that installed this copy. Accepted values: `npm`, `pnpm`, `bun`, `yarn`. |
121+
122+
</CommandTable>
123+
106124
## sim chat
107125

108126
Ask Sim and print the reply

‎apps/docs/content/docs/cli/troubleshooting.mdx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ The docs track the current release, so a command that exists here and not in
9999
sim --version
100100
```
101101

102-
Then upgrade with the package manager you installed it with — using a different
102+
Run `sim update` to update the active global installation. If this older release
103+
does not yet have the `update` command, upgrade with the package manager you installed it with — using a different
103104
one installs a second copy instead of replacing the executable on your `PATH`:
104105

105106
<Tabs items={['npm', 'pnpm', 'bun', 'Yarn Classic']}>
@@ -125,9 +126,10 @@ one installs a second copy instead of replacing the executable on your `PATH`:
125126
</Tab>
126127
</Tabs>
127128

128-
The CLI can also tell you this through a cached daily check on eligible
129-
invocations, and the command it prints already matches your installation. It
130-
stays quiet when stderr is redirected, in CI, and under `npm exec` or `npx`.
129+
The CLI also shows an optional update notice through a cached daily check on
130+
eligible interactive invocations. Your command continues, and you choose when to
131+
run `sim update`. Checks stay quiet when stderr is redirected, in CI, and under
132+
`npm exec` or `npx`.
131133

132134
## An update notice appears in output I am parsing
133135

‎apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function HeroToolCallItem({
2323
<ActivityStatus
2424
label={getToolStatusDisplayTitle(displayTitle, status, toolName)}
2525
isActive={status === 'executing'}
26-
icon={Icon && <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' />}
26+
icon={<Icon className='size-full' />}
2727
/>
2828
)
2929
return renderStatus ? renderStatus(activity) : activity
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
const mocks = vi.hoisted(() => ({
7+
defineInternalJsonRoute: vi.fn(() => vi.fn()),
8+
signOut: vi.fn(),
9+
}))
10+
11+
vi.mock('@/lib/api/server/routes', () => ({
12+
defineInternalJsonRoute: mocks.defineInternalJsonRoute,
13+
internalOrchestrationErrorPolicy: { project: vi.fn(), unhandled: vi.fn() },
14+
internalRateLimits: { none: vi.fn(() => ({ kind: 'none' })) },
15+
internalSessionAuth: { authenticate: vi.fn() },
16+
}))
17+
18+
vi.mock('@/lib/auth', () => ({
19+
auth: { api: { signOut: mocks.signOut } },
20+
getSession: vi.fn(),
21+
}))
22+
23+
vi.mock('@/lib/users/application/delete-account', () => ({
24+
deleteAccountUseCase: { execute: vi.fn() },
25+
previewAccountDeletionUseCase: { execute: vi.fn() },
26+
}))
27+
28+
vi.mock('@/lib/users/application/operations', () => ({
29+
userAccountOperations: { delete: { id: 'user.delete' }, previewDeletion: { id: 'user.preview' } },
30+
}))
31+
32+
import '@/app/api/users/me/deletion/route'
33+
34+
type RouteOptions = {
35+
finalizeResponse?: (args: { request: Request }) => Promise<{ headers?: HeadersInit }>
36+
}
37+
38+
/** Captured at import time; the route registers itself once when the module loads. */
39+
const routeOptions = mocks.defineInternalJsonRoute.mock.calls.map((call) => call[0] as RouteOptions)
40+
41+
describe('POST /api/users/me/deletion', () => {
42+
beforeEach(() => {
43+
mocks.signOut.mockReset()
44+
})
45+
46+
/**
47+
* The session row is deleted with the account, but the signed cookie cache
48+
* still authenticates the browser for its TTL; the response must clear it.
49+
*/
50+
it('clears the session cookies on the deletion response', async () => {
51+
const options = routeOptions.find(
52+
(candidate) => typeof candidate.finalizeResponse === 'function'
53+
)
54+
expect(options?.finalizeResponse).toBeDefined()
55+
56+
const cleared = new Headers([['set-cookie', 'better-auth.session_token=; Max-Age=0']])
57+
mocks.signOut.mockResolvedValue({ headers: cleared, response: { success: true } })
58+
const request = new Request('http://localhost/api/users/me/deletion', {
59+
method: 'POST',
60+
headers: { cookie: 'better-auth.session_token=abc' },
61+
})
62+
63+
const finalization = await options!.finalizeResponse!({ request })
64+
65+
expect(mocks.signOut).toHaveBeenCalledWith({ headers: request.headers, returnHeaders: true })
66+
expect(finalization.headers).toBe(cleared)
67+
})
68+
69+
it('never fails a completed deletion because the cookies could not be cleared', async () => {
70+
const options = routeOptions.find(
71+
(candidate) => typeof candidate.finalizeResponse === 'function'
72+
)
73+
mocks.signOut.mockRejectedValue(new Error('sign-out unavailable'))
74+
75+
await expect(
76+
options!.finalizeResponse!({
77+
request: new Request('http://localhost/api/users/me/deletion', { method: 'POST' }),
78+
})
79+
).resolves.toEqual({})
80+
})
81+
})

‎apps/sim/app/api/users/me/deletion/route.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
import { createLogger } from '@sim/logger'
12
import { deleteAccountContract, getAccountDeletionPlanContract } from '@/lib/api/contracts'
23
import {
34
defineInternalJsonRoute,
45
internalOrchestrationErrorPolicy,
56
internalRateLimits,
67
internalSessionAuth,
78
} from '@/lib/api/server/routes'
9+
import { auth } from '@/lib/auth'
810
import {
911
deleteAccountUseCase,
1012
previewAccountDeletionUseCase,
1113
} from '@/lib/users/application/delete-account'
1214
import { userAccountOperations } from '@/lib/users/application/operations'
1315

16+
const logger = createLogger('AccountDeletionRoute')
17+
1418
export const dynamic = 'force-dynamic'
1519

1620
export const GET = defineInternalJsonRoute({
@@ -39,4 +43,19 @@ export const POST = defineInternalJsonRoute({
3943
mapInput: ({ body }) => ({ confirmEmail: body.confirmEmail }),
4044
useCase: deleteAccountUseCase,
4145
present: () => ({ success: true as const }),
46+
/**
47+
* The session row is gone, but the signed cookie cache authenticates this
48+
* browser for up to five more minutes. Clear the cookies on the deletion
49+
* response itself so nothing the page does afterwards can carry them.
50+
*/
51+
finalizeResponse: async ({ request }) => {
52+
try {
53+
const { headers } = await auth.api.signOut({ headers: request.headers, returnHeaders: true })
54+
return { headers }
55+
} catch (error) {
56+
/** The account is gone either way; the client's own sign-out and full reload still drop the cookie. */
57+
logger.warn('Could not clear session cookies after account deletion', { error })
58+
return {}
59+
}
60+
},
4261
})

‎apps/sim/app/api/workspaces/route.test.ts‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,21 @@ vi.mock('@sim/audit', () => ({
4747
vi.mock('@/lib/workspaces/policy', async () => {
4848
class WorkspaceCreationCapabilityWithheldError extends Error {}
4949
class WorkspaceCreationContextChangedError extends Error {}
50+
class WorkspaceOwnerMissingError extends Error {}
5051
return {
5152
getWorkspaceCreationPolicy: mockGetWorkspaceCreationPolicy,
5253
WorkspaceCreationCapabilityWithheldError,
5354
WorkspaceCreationContextChangedError,
55+
WorkspaceOwnerMissingError,
5456
}
5557
})
5658

57-
import { WorkspaceCreationCapabilityWithheldError } from '@/lib/workspaces/policy'
58-
import { POST } from '@/app/api/workspaces/route'
59+
import { listWorkspacesForViewer } from '@/lib/workspaces/list'
60+
import {
61+
WorkspaceCreationCapabilityWithheldError,
62+
WorkspaceOwnerMissingError,
63+
} from '@/lib/workspaces/policy'
64+
import { GET, POST } from '@/app/api/workspaces/route'
5965

6066
function createRequest() {
6167
return createMockRequest('POST', { name: 'New workspace' })
@@ -116,4 +122,27 @@ describe('POST /api/workspaces capability refusal', () => {
116122
expect(body.error).toBe('Your organization subscription is inactive.')
117123
expect(body.details).toBeUndefined()
118124
})
125+
126+
/**
127+
* After account deletion the browser's cached session cookie stays valid for
128+
* a few minutes, and the next list load finds no workspaces and tries to
129+
* create the default one for a user who no longer exists.
130+
*/
131+
it('answers a default-workspace insert for a deleted user with 401', async () => {
132+
mockGetSession.mockResolvedValue({ user: { id: 'user-1', name: 'Gone' } })
133+
vi.mocked(listWorkspacesForViewer).mockResolvedValue({
134+
workspaces: [],
135+
lastActiveWorkspaceId: null,
136+
pinnedWorkspaceIds: [],
137+
creationPolicy: { canCreate: true, organizationId: null, billedAccountUserId: 'user-1' },
138+
} as never)
139+
mockCreateWorkspace.mockRejectedValue(new WorkspaceOwnerMissingError('user-1'))
140+
141+
const response = await GET(
142+
createMockRequest('GET', undefined, undefined, 'http://localhost/api/workspaces?scope=active')
143+
)
144+
145+
expect(response.status).toBe(401)
146+
await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' })
147+
})
119148
})

‎apps/sim/app/api/workspaces/route.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
getWorkspaceCreationPolicy,
2020
WorkspaceCreationCapabilityWithheldError,
2121
WorkspaceCreationContextChangedError,
22+
WorkspaceOwnerMissingError,
2223
} from '@/lib/workspaces/policy'
2324

2425
const logger = createLogger('Workspaces')
@@ -87,6 +88,10 @@ export const GET = withRouteHandler(async (request: Request) => {
8788
})
8889
return NextResponse.json(refreshedPayload)
8990
}
91+
/** A cached session cookie outlived the account it belongs to. */
92+
if (error instanceof WorkspaceOwnerMissingError) {
93+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
94+
}
9095
throw error
9196
}
9297

@@ -207,6 +212,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
207212
{ status: 409 }
208213
)
209214
}
215+
/** A cached session cookie outlived the account it belongs to. */
216+
if (error instanceof WorkspaceOwnerMissingError) {
217+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
218+
}
210219
/**
211220
* A lock timeout is contention, not a fault: creation serializes on the
212221
* organization's mutation locks and now also on `permission_group:<org>`,

0 commit comments

Comments
 (0)