@shipfox/client-runners 22.0.3 → 23.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-runners",
3
3
  "license": "MIT",
4
- "version": "22.0.3",
4
+ "version": "23.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,11 +27,11 @@
27
27
  "dependencies": {
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "@tanstack/react-form": "^1.32.0",
30
- "@shipfox/api-runners-dto": "13.0.0",
31
30
  "@shipfox/client-api": "6.0.1",
32
- "@shipfox/client-shell": "22.0.3",
31
+ "@shipfox/client-shell": "23.0.0",
33
32
  "@shipfox/client-ui": "22.0.3",
34
- "@shipfox/react-ui": "2.2.0"
33
+ "@shipfox/react-ui": "2.2.0",
34
+ "@shipfox/api-runners-dto": "14.0.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@tanstack/react-query": "^5.101.0",
@@ -86,7 +86,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
86
86
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
87
87
  const request = input as Request;
88
88
  if (request.url.endsWith('/provisioners/active')) {
89
- return Promise.resolve(jsonResponse({provisioners: []}));
89
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
90
90
  }
91
91
  return Promise.resolve(jsonResponse({tokens: []}));
92
92
  });
@@ -106,7 +106,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
106
106
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
107
107
  const request = input as Request;
108
108
  if (request.url.endsWith('/provisioners/active')) {
109
- return Promise.resolve(jsonResponse({provisioners: []}));
109
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
110
110
  }
111
111
  if (request.method === 'POST' && request.url.endsWith('/provisioners/tokens')) {
112
112
  tokens = [provisionerToken({name: 'Docker provisioner'})];
@@ -145,7 +145,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
145
145
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
146
146
  const request = input as Request;
147
147
  if (request.url.endsWith('/provisioners/active')) {
148
- return Promise.resolve(jsonResponse({provisioners: []}));
148
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
149
149
  }
150
150
  return Promise.resolve(
151
151
  jsonResponse({
@@ -181,7 +181,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
181
181
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
182
182
  const request = input as Request;
183
183
  if (request.url.endsWith('/provisioners/active')) {
184
- return Promise.resolve(jsonResponse({provisioners: []}));
184
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
185
185
  }
186
186
  return Promise.resolve(jsonResponse({tokens: [provisionerToken({name: tokenName})]}));
187
187
  });
@@ -206,7 +206,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
206
206
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
207
207
  const request = input as Request;
208
208
  if (request.url.endsWith('/provisioners/active')) {
209
- return Promise.resolve(jsonResponse({provisioners: []}));
209
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
210
210
  }
211
211
  if (request.method === 'POST' && request.url.endsWith('/provisioners/tokens')) {
212
212
  return Promise.resolve(
@@ -241,6 +241,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
241
241
  return Promise.resolve(
242
242
  jsonResponse({
243
243
  provisioners: [activeProvisioner({id: activeId, name: 'Active provisioner'})],
244
+ installation_runners: 'none',
244
245
  }),
245
246
  );
246
247
  }
@@ -306,7 +307,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
306
307
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
307
308
  const request = input as Request;
308
309
  if (request.url.endsWith('/provisioners/active')) {
309
- return Promise.resolve(jsonResponse({provisioners: []}));
310
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
310
311
  }
311
312
  if (request.method === 'POST' && request.url.includes('/revoke')) {
312
313
  tokens = [];
@@ -339,7 +340,7 @@ describe('WorkspaceProvisionerTokensSettingsSection', () => {
339
340
  const fetchImpl = vi.fn((input: RequestInfo | URL) => {
340
341
  const request = input as Request;
341
342
  if (request.url.endsWith('/provisioners/active')) {
342
- return Promise.resolve(jsonResponse({provisioners: []}));
343
+ return Promise.resolve(jsonResponse({provisioners: [], installation_runners: 'none'}));
343
344
  }
344
345
  if (request.method === 'POST' && request.url.includes('/revoke')) {
345
346
  return Promise.resolve(
@@ -278,6 +278,7 @@ function fetchForScenario(scenario: Scenario): typeof fetch {
278
278
  last_seen_at: NOW,
279
279
  },
280
280
  ],
281
+ installation_runners: 'none',
281
282
  }),
282
283
  );
283
284
  }
package/src/core/token.ts CHANGED
@@ -22,6 +22,15 @@ export interface ActiveProvisioner {
22
22
  lastSeenAt: string;
23
23
  }
24
24
 
25
+ export type InstallationRunnersStatus = 'managed' | 'none';
26
+
27
+ export type ActiveProvisioners = ActiveProvisioner[];
28
+
29
+ export interface ActiveProvisionersResponse {
30
+ provisioners: ActiveProvisioners;
31
+ installationRunners: InstallationRunnersStatus;
32
+ }
33
+
25
34
  export interface CreatedManualRegistrationToken {
26
35
  token: string;
27
36
  id: string;
@@ -2,6 +2,7 @@ import {configureApiClient} from '@shipfox/client-api';
2
2
  import {
3
3
  createProvisionerToken,
4
4
  listActiveProvisioners,
5
+ listActiveProvisionersResponse,
5
6
  listProvisionerTokens,
6
7
  revokeProvisionerToken,
7
8
  } from './provisioner-tokens.js';
@@ -105,7 +106,7 @@ describe('provisioner token transports', () => {
105
106
  expect(request.method).toBe('POST');
106
107
  });
107
108
 
108
- test('lists active provisioners', async () => {
109
+ test('keeps listActiveProvisioners array-compatible', async () => {
109
110
  const fetchImpl = vi.fn().mockResolvedValue(
110
111
  jsonResponse({
111
112
  provisioners: [
@@ -116,6 +117,7 @@ describe('provisioner token transports', () => {
116
117
  last_seen_at: '2026-05-08T01:00:00.000Z',
117
118
  },
118
119
  ],
120
+ installation_runners: 'managed',
119
121
  }),
120
122
  );
121
123
  configureApiClient({fetchImpl});
@@ -124,9 +126,21 @@ describe('provisioner token transports', () => {
124
126
 
125
127
  const request = fetchImpl.mock.calls[0]?.[0] as Request;
126
128
  expect(result).toHaveLength(1);
129
+ expect(result[0]?.name).toBe('Docker provisioner');
127
130
  expect(request.url).toBe(
128
131
  `https://api.example.test/workspaces/${workspaceId}/provisioners/active`,
129
132
  );
130
133
  expect(request.method).toBe('GET');
131
134
  });
135
+
136
+ test('exposes installation runners status through the response adapter', async () => {
137
+ const fetchImpl = vi
138
+ .fn()
139
+ .mockResolvedValue(jsonResponse({provisioners: [], installation_runners: 'managed'}));
140
+ configureApiClient({fetchImpl});
141
+
142
+ const result = await listActiveProvisionersResponse({workspaceId});
143
+
144
+ expect(result).toEqual({provisioners: [], installationRunners: 'managed'});
145
+ });
132
146
  });
@@ -6,20 +6,22 @@ import {
6
6
  } from '@shipfox/api-runners-dto';
7
7
  import {checkedApiRequest} from '@shipfox/client-api';
8
8
  import {
9
- type FetchQueryOptions,
10
9
  queryOptions,
10
+ type UseQueryOptions,
11
11
  useMutation,
12
12
  useQuery,
13
13
  useQueryClient,
14
14
  } from '@tanstack/react-query';
15
15
  import type {
16
- ActiveProvisioner,
16
+ ActiveProvisioners,
17
+ ActiveProvisionersResponse,
17
18
  CreatedProvisionerToken,
18
19
  CreateTokenCommand,
20
+ InstallationRunnersStatus,
19
21
  ProvisionerToken,
20
22
  } from '#core/token.js';
21
23
  import {
22
- toActiveProvisioner,
24
+ toActiveProvisionersResponse,
23
25
  toCreatedProvisionerToken,
24
26
  toCreateTokenBody,
25
27
  toProvisionerToken,
@@ -34,17 +36,24 @@ export const provisionerTokenQueryKeys = {
34
36
  [...provisionerTokenQueryKeys.all, 'provisioners', workspaceId] as const,
35
37
  };
36
38
 
37
- type ProvisionerTokensQueryOptions = FetchQueryOptions<
39
+ type ProvisionerTokensQueryOptions = UseQueryOptions<
38
40
  ProvisionerToken[],
39
41
  Error,
40
42
  ProvisionerToken[],
41
43
  ReturnType<typeof provisionerTokenQueryKeys.list>
42
44
  >;
43
45
 
44
- type ActiveProvisionersQueryOptions = FetchQueryOptions<
45
- ActiveProvisioner[],
46
+ type ActiveProvisionersQueryOptions = UseQueryOptions<
47
+ ActiveProvisionersResponse,
46
48
  Error,
47
- ActiveProvisioner[],
49
+ ActiveProvisioners,
50
+ ReturnType<typeof provisionerTokenQueryKeys.active>
51
+ >;
52
+
53
+ type InstallationRunnersStatusQueryOptions = UseQueryOptions<
54
+ ActiveProvisionersResponse,
55
+ Error,
56
+ InstallationRunnersStatus,
48
57
  ReturnType<typeof provisionerTokenQueryKeys.active>
49
58
  >;
50
59
 
@@ -93,19 +102,33 @@ export async function revokeProvisionerToken({
93
102
  return toProvisionerToken(response);
94
103
  }
95
104
 
96
- export async function listActiveProvisioners({
105
+ export async function listActiveProvisionersResponse({
97
106
  workspaceId,
98
107
  signal,
99
108
  }: {
100
109
  workspaceId: string;
101
110
  signal?: AbortSignal;
102
- }): Promise<ActiveProvisioner[]> {
111
+ }): Promise<ActiveProvisionersResponse> {
103
112
  const response = await checkedApiRequest(
104
113
  listActiveProvisionersResponseSchema,
105
114
  `/workspaces/${workspaceId}/provisioners/active`,
106
115
  {signal},
107
116
  );
108
- return response.provisioners.map(toActiveProvisioner);
117
+ return toActiveProvisionersResponse(response);
118
+ }
119
+
120
+ export async function listActiveProvisioners({
121
+ workspaceId,
122
+ signal,
123
+ }: {
124
+ workspaceId: string;
125
+ signal?: AbortSignal;
126
+ }): Promise<ActiveProvisioners> {
127
+ const response = await listActiveProvisionersResponse({
128
+ workspaceId,
129
+ ...(signal ? {signal} : {}),
130
+ });
131
+ return response.provisioners;
109
132
  }
110
133
 
111
134
  export function provisionerTokensQueryOptions(workspaceId: string): ProvisionerTokensQueryOptions {
@@ -120,12 +143,27 @@ export function provisionerTokensQueryOptions(workspaceId: string): ProvisionerT
120
143
  export function activeProvisionersQueryOptions(
121
144
  workspaceId: string,
122
145
  ): ActiveProvisionersQueryOptions {
123
- return queryOptions({
146
+ return {
124
147
  queryKey: provisionerTokenQueryKeys.active(workspaceId),
125
- queryFn: ({signal}) => listActiveProvisioners({workspaceId, signal}),
148
+ queryFn: ({signal}) =>
149
+ listActiveProvisionersResponse({workspaceId, ...(signal ? {signal} : {})}),
150
+ select: (response) => response.provisioners,
126
151
  refetchInterval: PROVISIONER_TOKEN_REFETCH_INTERVAL_MS,
127
152
  refetchIntervalInBackground: false,
128
- });
153
+ };
154
+ }
155
+
156
+ export function installationRunnersStatusQueryOptions(
157
+ workspaceId: string,
158
+ ): InstallationRunnersStatusQueryOptions {
159
+ return {
160
+ queryKey: provisionerTokenQueryKeys.active(workspaceId),
161
+ queryFn: ({signal}) =>
162
+ listActiveProvisionersResponse({workspaceId, ...(signal ? {signal} : {})}),
163
+ select: (response) => response.installationRunners,
164
+ refetchInterval: PROVISIONER_TOKEN_REFETCH_INTERVAL_MS,
165
+ refetchIntervalInBackground: false,
166
+ };
129
167
  }
130
168
 
131
169
  export function useProvisionerTokensQuery(workspaceId: string | undefined) {
@@ -136,12 +174,29 @@ export function useProvisionerTokensQuery(workspaceId: string | undefined) {
136
174
  }
137
175
 
138
176
  export function useActiveProvisionersQuery(workspaceId: string | undefined) {
139
- return useQuery({
177
+ return useQuery<
178
+ ActiveProvisionersResponse,
179
+ Error,
180
+ ActiveProvisioners,
181
+ ReturnType<typeof provisionerTokenQueryKeys.active>
182
+ >({
140
183
  ...activeProvisionersQueryOptions(workspaceId ?? ''),
141
184
  enabled: Boolean(workspaceId),
142
185
  });
143
186
  }
144
187
 
188
+ export function useInstallationRunnersStatusQuery(workspaceId: string | undefined) {
189
+ return useQuery<
190
+ ActiveProvisionersResponse,
191
+ Error,
192
+ InstallationRunnersStatus,
193
+ ReturnType<typeof provisionerTokenQueryKeys.active>
194
+ >({
195
+ ...installationRunnersStatusQueryOptions(workspaceId ?? ''),
196
+ enabled: Boolean(workspaceId),
197
+ });
198
+ }
199
+
145
200
  export function useCreateProvisionerTokenMutation(workspaceId: string) {
146
201
  const queryClient = useQueryClient();
147
202
  return useMutation({
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  toActiveProvisioner,
3
+ toActiveProvisionersResponse,
3
4
  toCreatedManualRegistrationToken,
4
5
  toCreatedProvisionerToken,
5
6
  toCreateTokenBody,
@@ -84,6 +85,34 @@ describe('toActiveProvisioner', () => {
84
85
  });
85
86
  });
86
87
 
88
+ describe('toActiveProvisionersResponse', () => {
89
+ test('maps the transport response to its package-owned model', () => {
90
+ const result = toActiveProvisionersResponse({
91
+ provisioners: [
92
+ {
93
+ id: '33333333-3333-4333-8333-333333333333',
94
+ name: 'Docker provisioner',
95
+ prefix: 'sf_pt_test',
96
+ last_seen_at: '2026-07-22T09:00:00.000Z',
97
+ },
98
+ ],
99
+ installation_runners: 'none',
100
+ });
101
+
102
+ expect(result).toEqual({
103
+ provisioners: [
104
+ {
105
+ id: '33333333-3333-4333-8333-333333333333',
106
+ name: 'Docker provisioner',
107
+ prefix: 'sf_pt_test',
108
+ lastSeenAt: '2026-07-22T09:00:00.000Z',
109
+ },
110
+ ],
111
+ installationRunners: 'none',
112
+ });
113
+ });
114
+ });
115
+
87
116
  describe('toCreatedManualRegistrationToken', () => {
88
117
  test('maps a raw-token creation response to its package-owned model', () => {
89
118
  const result = toCreatedManualRegistrationToken({
@@ -2,18 +2,19 @@ import type {
2
2
  ActiveProvisionerDto,
3
3
  CreateManualRegistrationTokenResponseDto,
4
4
  CreateProvisionerTokenResponseDto,
5
+ ListActiveProvisionersResponseDto,
5
6
  ManualRegistrationTokenDto,
6
7
  ProvisionerTokenDto,
7
8
  } from '@shipfox/api-runners-dto';
8
9
  import type {
9
10
  ActiveProvisioner,
11
+ ActiveProvisionersResponse,
10
12
  CreatedManualRegistrationToken,
11
13
  CreatedProvisionerToken,
12
14
  CreateTokenCommand,
13
15
  ManualRegistrationToken,
14
16
  ProvisionerToken,
15
17
  } from '#core/token.js';
16
-
17
18
  export function toCreateTokenBody(command: CreateTokenCommand) {
18
19
  return {
19
20
  ...(command.name ? {name: command.name} : {}),
@@ -51,6 +52,15 @@ export function toActiveProvisioner(dto: ActiveProvisionerDto): ActiveProvisione
51
52
  return {id: dto.id, name: dto.name, prefix: dto.prefix, lastSeenAt: dto.last_seen_at};
52
53
  }
53
54
 
55
+ export function toActiveProvisionersResponse(
56
+ dto: ListActiveProvisionersResponseDto,
57
+ ): ActiveProvisionersResponse {
58
+ return {
59
+ provisioners: dto.provisioners.map(toActiveProvisioner),
60
+ installationRunners: dto.installation_runners,
61
+ };
62
+ }
63
+
54
64
  export function toCreatedManualRegistrationToken(
55
65
  dto: CreateManualRegistrationTokenResponseDto,
56
66
  ): CreatedManualRegistrationToken {