Skip to content

Commit 26fe609

Browse files
os-zhuangclaude
andauthored
feat(client): pass overwrite through packages.install for upgrade flows (#3007)
POST /packages now 409s when the manifest id is already installed (#2971, duplicate-id guard). The SDK's packages.install() had no way to opt into the intentional-overwrite escape hatch, so any upgrade / re-install flow built on it would hit the 409 with no recourse. Add `overwrite?: boolean` to the options bag and pass it through the POST body only when explicitly set — the default request stays byte-identical so the server keeps rejecting duplicate ids. First SDK tests for packages.install (default omits the flag; overwrite:true carries it). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 13c0a12 commit 26fe609

2 files changed

Lines changed: 36 additions & 2 deletions

File tree

packages/client/src/client.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,3 +1179,28 @@ describe('HTTP error shaping', () => {
11791179
expect(caught.httpStatus).toBe(500);
11801180
});
11811181
});
1182+
1183+
describe('packages.install', () => {
1184+
const MANIFEST = { id: 'com.acme.crm', name: 'Acme CRM', version: '1.0.0', type: 'app' };
1185+
1186+
it('POSTs the manifest and omits `overwrite` unless requested', async () => {
1187+
const { client, fetchMock } = createMockClient({ success: true, data: { package: { manifest: MANIFEST } } });
1188+
await client.packages.install(MANIFEST, { enableOnInstall: true });
1189+
1190+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3000/api/v1/packages', expect.any(Object));
1191+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
1192+
expect(body.manifest).toEqual(MANIFEST);
1193+
expect(body.enableOnInstall).toBe(true);
1194+
// Duplicate-id guard semantics: never send `overwrite` implicitly —
1195+
// the server must keep 409ing on an existing id by default.
1196+
expect('overwrite' in body).toBe(false);
1197+
});
1198+
1199+
it('passes `overwrite: true` through for intentional upgrade / re-install', async () => {
1200+
const { client, fetchMock } = createMockClient({ success: true, data: { package: { manifest: MANIFEST } } });
1201+
await client.packages.install(MANIFEST, { overwrite: true });
1202+
1203+
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
1204+
expect(body.overwrite).toBe(true);
1205+
});
1206+
});

packages/client/src/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,15 +592,24 @@ export class ObjectStackClient {
592592

593593
/**
594594
* Install a new package from its manifest.
595-
*/
596-
install: async (manifest: any, options?: { settings?: Record<string, any>; enableOnInstall?: boolean }) => {
595+
*
596+
* By default the server rejects a manifest whose `id` is already
597+
* installed with **409 Conflict** (duplicate-id guard) instead of
598+
* silently overwriting the existing package. Intentional upgrade /
599+
* re-install flows opt back in with `overwrite: true`.
600+
*/
601+
install: async (
602+
manifest: any,
603+
options?: { settings?: Record<string, any>; enableOnInstall?: boolean; overwrite?: boolean },
604+
) => {
597605
const route = this.getRoute('packages');
598606
const res = await this.fetch(`${this.baseUrl}${route}`, {
599607
method: 'POST',
600608
body: JSON.stringify({
601609
manifest,
602610
settings: options?.settings,
603611
enableOnInstall: options?.enableOnInstall,
612+
...(options?.overwrite !== undefined ? { overwrite: options.overwrite } : {}),
604613
}),
605614
});
606615
return this.unwrapResponse<{ package: any; message?: string }>(res);

0 commit comments

Comments
 (0)