@shipfox/client-onboarding 22.0.2 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-setup-route.d.ts","sourceRoot":"","sources":["../src/workspace-setup-route.ts"],"names":[],"mappings":"AASA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACzB,MAAM,+BAA+B,CAAC;AAMvC,YAAY,EAAC,0BAA0B,EAAC,MAAM,+BAA+B,CAAC;AAE9E,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,WAAW,EACX,aAAa,EACb,QAAQ,GACT,EAAE,0BAA0B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsE3D"}
1
+ {"version":3,"file":"workspace-setup-route.d.ts","sourceRoot":"","sources":["../src/workspace-setup-route.ts"],"names":[],"mappings":"AAUA,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACzB,MAAM,+BAA+B,CAAC;AAMvC,YAAY,EAAC,0BAA0B,EAAC,MAAM,+BAA+B,CAAC;AAE9E,wBAAsB,uBAAuB,CAAC,EAC5C,WAAW,EACX,WAAW,EACX,aAAa,EACb,QAAQ,GACT,EAAE,0BAA0B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsE3D"}
@@ -1,4 +1,4 @@
1
- import { isModelProviderOnboardingDismissed, modelProviderConfigsQueryOptions } from '@shipfox/client-agent';
1
+ import { isModelProviderOnboardingDismissed, modelProviderCatalogQueryOptions, modelProviderConfigsQueryOptions } from '@shipfox/client-agent';
2
2
  import { sourceConnectionsQueryOptions } from '@shipfox/client-integrations';
3
3
  import { projectExistenceQueryOptions } from '@shipfox/client-projects';
4
4
  import { userWorkspacesQueryOptions, WorkspaceSetupLoadError } from '@shipfox/client-shell/runtime';
@@ -118,14 +118,21 @@ async function fetchWorkspaceSourceConnections(queryClient, workspaceId) {
118
118
  }
119
119
  async function hasHandledModelProviderOnboarding(queryClient, workspaceId) {
120
120
  if (isModelProviderOnboardingDismissed(workspaceId)) return true;
121
- const options = modelProviderConfigsQueryOptions(workspaceId);
122
- try {
123
- const configs = await queryClient.fetchQuery(options);
121
+ const catalogOptions = modelProviderCatalogQueryOptions();
122
+ const configOptions = modelProviderConfigsQueryOptions(workspaceId);
123
+ const [catalogResult, configsResult] = await Promise.allSettled([
124
+ queryClient.fetchQuery(catalogOptions),
125
+ queryClient.fetchQuery(configOptions)
126
+ ]);
127
+ if (catalogResult.status === 'fulfilled' && catalogResult.value.workspaceProviders === 'disabled') {
128
+ return true;
129
+ }
130
+ if (configsResult.status === 'fulfilled') {
131
+ const configs = configsResult.value;
124
132
  return configs.configs.length > 0;
125
- } catch {
126
- const cached = queryClient.getQueryData(options.queryKey);
127
- return cached?.configs.length !== 0;
128
133
  }
134
+ const cached = queryClient.getQueryData(configOptions.queryKey);
135
+ return cached?.configs.length !== 0;
129
136
  }
130
137
  function normalizePath(pathname) {
131
138
  if (pathname === '/') return pathname;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workspace-setup-route.ts"],"sourcesContent":["import {\n isModelProviderOnboardingDismissed,\n modelProviderConfigsQueryOptions,\n} from '@shipfox/client-agent';\nimport {\n type IntegrationConnection,\n sourceConnectionsQueryOptions,\n} from '@shipfox/client-integrations';\nimport {projectExistenceQueryOptions} from '@shipfox/client-projects';\nimport {\n userWorkspacesQueryOptions,\n WorkspaceSetupLoadError,\n type WorkspaceSetupRouteOptions,\n type WorkspaceSetupState,\n} from '@shipfox/client-shell/runtime';\nimport type {QueryClient} from '@tanstack/react-query';\nimport {redirect} from '@tanstack/react-router';\n\nconst TRAILING_SLASHES_RE = /\\/+$/u;\n\nexport type {WorkspaceSetupRouteOptions} from '@shipfox/client-shell/runtime';\n\nexport async function loadWorkspaceSetupRoute({\n queryClient,\n workspaceId,\n workspaceSlug,\n pathname,\n}: WorkspaceSetupRouteOptions): Promise<WorkspaceSetupState> {\n const workspace = await fetchWorkspaceSummary(queryClient, workspaceId);\n const workspaceStatus = workspace?.status;\n switch (workspaceStatus) {\n case undefined:\n case 'active':\n break;\n case 'suspended':\n case 'deleted':\n return {hideProjectNavigation: true, unavailable: true};\n default:\n return assertNever(workspaceStatus);\n }\n\n const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);\n const normalizedPathname = normalizePath(pathname);\n\n if (projects.projects.length > 0) {\n if (isIntegrationsIndexPath(normalizedPathname, workspaceSlug)) {\n throw redirect({\n to: '/w/$workspaceSlug/settings/integrations',\n params: {workspaceSlug},\n replace: true,\n });\n }\n\n return {hideProjectNavigation: false};\n }\n\n const sourceConnections = await fetchWorkspaceSourceConnections(queryClient, workspaceId);\n const hasSourceConnection = sourceConnections.length > 0;\n\n if (!hasSourceConnection) {\n if (isIntegrationSetupPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/w/$workspaceSlug/integrations',\n params: {workspaceSlug},\n replace: true,\n });\n }\n\n if (isAgentSettingsPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n const providerHandled = await hasHandledModelProviderOnboarding(queryClient, workspaceId);\n if (!providerHandled) {\n if (isModelProviderOnboardingPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/w/$workspaceSlug/model-provider',\n params: {workspaceSlug},\n replace: true,\n });\n }\n\n if (isProjectCreationPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/w/$workspaceSlug/projects/new',\n params: {workspaceSlug},\n replace: true,\n });\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unhandled workspace status: ${JSON.stringify(value)}`);\n}\n\nasync function fetchWorkspaceSummary(queryClient: QueryClient, workspaceId: string) {\n try {\n const result = await queryClient.fetchQuery(userWorkspacesQueryOptions());\n return result.memberships.find((workspace) => workspace.id === workspaceId);\n } catch (error) {\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceProjectExistence(queryClient: QueryClient, workspaceId: string) {\n const options = projectExistenceQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<{projects: unknown[]}>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceSourceConnections(queryClient: QueryClient, workspaceId: string) {\n const options = sourceConnectionsQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<IntegrationConnection[]>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function hasHandledModelProviderOnboarding(\n queryClient: QueryClient,\n workspaceId: string,\n): Promise<boolean> {\n if (isModelProviderOnboardingDismissed(workspaceId)) return true;\n\n const options = modelProviderConfigsQueryOptions(workspaceId);\n try {\n const configs = await queryClient.fetchQuery(options);\n return configs.configs.length > 0;\n } catch {\n const cached = queryClient.getQueryData<{configs: unknown[]}>(options.queryKey);\n return cached?.configs.length !== 0;\n }\n}\n\nfunction normalizePath(pathname: string) {\n if (pathname === '/') return pathname;\n return pathname.replace(TRAILING_SLASHES_RE, '');\n}\n\nfunction workspacePath(workspaceSlug: string, suffix: string) {\n return `/w/${workspaceSlug}${suffix}`;\n}\n\nfunction isIntegrationsIndexPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/integrations');\n}\n\nfunction isIntegrationSetupPath(pathname: string, workspaceSlug: string) {\n const basePath = workspacePath(workspaceSlug, '/integrations');\n return (\n pathname === basePath ||\n pathname.startsWith(`${basePath}/`) ||\n pathname === workspacePath(workspaceSlug, '/settings/integrations')\n );\n}\n\nfunction isProjectCreationPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/projects/new');\n}\n\nfunction isModelProviderOnboardingPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/model-provider');\n}\n\nfunction isAgentSettingsPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/settings/agents');\n}\n"],"names":["isModelProviderOnboardingDismissed","modelProviderConfigsQueryOptions","sourceConnectionsQueryOptions","projectExistenceQueryOptions","userWorkspacesQueryOptions","WorkspaceSetupLoadError","redirect","TRAILING_SLASHES_RE","loadWorkspaceSetupRoute","queryClient","workspaceId","workspaceSlug","pathname","workspace","fetchWorkspaceSummary","workspaceStatus","status","undefined","hideProjectNavigation","unavailable","assertNever","projects","fetchWorkspaceProjectExistence","normalizedPathname","normalizePath","length","isIntegrationsIndexPath","to","params","replace","sourceConnections","fetchWorkspaceSourceConnections","hasSourceConnection","isIntegrationSetupPath","isAgentSettingsPath","providerHandled","hasHandledModelProviderOnboarding","isModelProviderOnboardingPath","isProjectCreationPath","value","Error","JSON","stringify","result","fetchQuery","memberships","find","id","error","options","cached","getQueryData","queryKey","configs","workspacePath","suffix","basePath","startsWith"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/workspace-setup-route.ts"],"sourcesContent":["import {\n isModelProviderOnboardingDismissed,\n modelProviderCatalogQueryOptions,\n modelProviderConfigsQueryOptions,\n} from '@shipfox/client-agent';\nimport {\n type IntegrationConnection,\n sourceConnectionsQueryOptions,\n} from '@shipfox/client-integrations';\nimport {projectExistenceQueryOptions} from '@shipfox/client-projects';\nimport {\n userWorkspacesQueryOptions,\n WorkspaceSetupLoadError,\n type WorkspaceSetupRouteOptions,\n type WorkspaceSetupState,\n} from '@shipfox/client-shell/runtime';\nimport type {QueryClient} from '@tanstack/react-query';\nimport {redirect} from '@tanstack/react-router';\n\nconst TRAILING_SLASHES_RE = /\\/+$/u;\n\nexport type {WorkspaceSetupRouteOptions} from '@shipfox/client-shell/runtime';\n\nexport async function loadWorkspaceSetupRoute({\n queryClient,\n workspaceId,\n workspaceSlug,\n pathname,\n}: WorkspaceSetupRouteOptions): Promise<WorkspaceSetupState> {\n const workspace = await fetchWorkspaceSummary(queryClient, workspaceId);\n const workspaceStatus = workspace?.status;\n switch (workspaceStatus) {\n case undefined:\n case 'active':\n break;\n case 'suspended':\n case 'deleted':\n return {hideProjectNavigation: true, unavailable: true};\n default:\n return assertNever(workspaceStatus);\n }\n\n const projects = await fetchWorkspaceProjectExistence(queryClient, workspaceId);\n const normalizedPathname = normalizePath(pathname);\n\n if (projects.projects.length > 0) {\n if (isIntegrationsIndexPath(normalizedPathname, workspaceSlug)) {\n throw redirect({\n to: '/w/$workspaceSlug/settings/integrations',\n params: {workspaceSlug},\n replace: true,\n });\n }\n\n return {hideProjectNavigation: false};\n }\n\n const sourceConnections = await fetchWorkspaceSourceConnections(queryClient, workspaceId);\n const hasSourceConnection = sourceConnections.length > 0;\n\n if (!hasSourceConnection) {\n if (isIntegrationSetupPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/w/$workspaceSlug/integrations',\n params: {workspaceSlug},\n replace: true,\n });\n }\n\n if (isAgentSettingsPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n const providerHandled = await hasHandledModelProviderOnboarding(queryClient, workspaceId);\n if (!providerHandled) {\n if (isModelProviderOnboardingPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/w/$workspaceSlug/model-provider',\n params: {workspaceSlug},\n replace: true,\n });\n }\n\n if (isProjectCreationPath(normalizedPathname, workspaceSlug)) {\n return {hideProjectNavigation: true};\n }\n\n throw redirect({\n to: '/w/$workspaceSlug/projects/new',\n params: {workspaceSlug},\n replace: true,\n });\n}\n\nfunction assertNever(value: never): never {\n throw new Error(`Unhandled workspace status: ${JSON.stringify(value)}`);\n}\n\nasync function fetchWorkspaceSummary(queryClient: QueryClient, workspaceId: string) {\n try {\n const result = await queryClient.fetchQuery(userWorkspacesQueryOptions());\n return result.memberships.find((workspace) => workspace.id === workspaceId);\n } catch (error) {\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceProjectExistence(queryClient: QueryClient, workspaceId: string) {\n const options = projectExistenceQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<{projects: unknown[]}>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function fetchWorkspaceSourceConnections(queryClient: QueryClient, workspaceId: string) {\n const options = sourceConnectionsQueryOptions(workspaceId);\n\n try {\n return await queryClient.fetchQuery(options);\n } catch (error) {\n const cached = queryClient.getQueryData<IntegrationConnection[]>(options.queryKey);\n if (cached !== undefined) return cached;\n throw new WorkspaceSetupLoadError(error);\n }\n}\n\nasync function hasHandledModelProviderOnboarding(\n queryClient: QueryClient,\n workspaceId: string,\n): Promise<boolean> {\n if (isModelProviderOnboardingDismissed(workspaceId)) return true;\n\n const catalogOptions = modelProviderCatalogQueryOptions();\n const configOptions = modelProviderConfigsQueryOptions(workspaceId);\n const [catalogResult, configsResult] = await Promise.allSettled([\n queryClient.fetchQuery(catalogOptions),\n queryClient.fetchQuery(configOptions),\n ]);\n\n if (\n catalogResult.status === 'fulfilled' &&\n catalogResult.value.workspaceProviders === 'disabled'\n ) {\n return true;\n }\n\n if (configsResult.status === 'fulfilled') {\n const configs = configsResult.value;\n return configs.configs.length > 0;\n }\n\n const cached = queryClient.getQueryData<{configs: unknown[]}>(configOptions.queryKey);\n return cached?.configs.length !== 0;\n}\n\nfunction normalizePath(pathname: string) {\n if (pathname === '/') return pathname;\n return pathname.replace(TRAILING_SLASHES_RE, '');\n}\n\nfunction workspacePath(workspaceSlug: string, suffix: string) {\n return `/w/${workspaceSlug}${suffix}`;\n}\n\nfunction isIntegrationsIndexPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/integrations');\n}\n\nfunction isIntegrationSetupPath(pathname: string, workspaceSlug: string) {\n const basePath = workspacePath(workspaceSlug, '/integrations');\n return (\n pathname === basePath ||\n pathname.startsWith(`${basePath}/`) ||\n pathname === workspacePath(workspaceSlug, '/settings/integrations')\n );\n}\n\nfunction isProjectCreationPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/projects/new');\n}\n\nfunction isModelProviderOnboardingPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/model-provider');\n}\n\nfunction isAgentSettingsPath(pathname: string, workspaceSlug: string) {\n return pathname === workspacePath(workspaceSlug, '/settings/agents');\n}\n"],"names":["isModelProviderOnboardingDismissed","modelProviderCatalogQueryOptions","modelProviderConfigsQueryOptions","sourceConnectionsQueryOptions","projectExistenceQueryOptions","userWorkspacesQueryOptions","WorkspaceSetupLoadError","redirect","TRAILING_SLASHES_RE","loadWorkspaceSetupRoute","queryClient","workspaceId","workspaceSlug","pathname","workspace","fetchWorkspaceSummary","workspaceStatus","status","undefined","hideProjectNavigation","unavailable","assertNever","projects","fetchWorkspaceProjectExistence","normalizedPathname","normalizePath","length","isIntegrationsIndexPath","to","params","replace","sourceConnections","fetchWorkspaceSourceConnections","hasSourceConnection","isIntegrationSetupPath","isAgentSettingsPath","providerHandled","hasHandledModelProviderOnboarding","isModelProviderOnboardingPath","isProjectCreationPath","value","Error","JSON","stringify","result","fetchQuery","memberships","find","id","error","options","cached","getQueryData","queryKey","catalogOptions","configOptions","catalogResult","configsResult","Promise","allSettled","workspaceProviders","configs","workspacePath","suffix","basePath","startsWith"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-onboarding",
3
3
  "license": "MIT",
4
- "version": "22.0.2",
4
+ "version": "23.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -18,10 +18,10 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@swc/helpers": "^0.5.17",
21
- "@shipfox/client-agent": "22.0.2",
22
- "@shipfox/client-integrations": "22.0.2",
23
- "@shipfox/client-projects": "22.0.2",
24
- "@shipfox/client-shell": "22.0.2"
21
+ "@shipfox/client-agent": "23.0.0",
22
+ "@shipfox/client-integrations": "23.0.0",
23
+ "@shipfox/client-projects": "23.0.0",
24
+ "@shipfox/client-shell": "23.0.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "@tanstack/react-query": "^5.101.0",
@@ -29,10 +29,15 @@
29
29
  "react": "^19.0.0",
30
30
  "react-dom": "^19.0.0"
31
31
  },
32
+ "imports": {
33
+ "#*": "./dist/*"
34
+ },
32
35
  "scripts": {
33
36
  "build": "shipfox-swc",
34
37
  "check": "shipfox-biome-check",
35
38
  "check:fix": "shipfox-biome-check --write",
39
+ "storybook": "storybook dev -p 6015",
40
+ "storybook:build": "storybook build -o storybook-static",
36
41
  "test": "shipfox-vitest-run",
37
42
  "type": "shipfox-tsc-check",
38
43
  "type:emit": "shipfox-tsc-emit"
@@ -0,0 +1,277 @@
1
+ import type {
2
+ IntegrationCapability,
3
+ IntegrationConnection,
4
+ IntegrationProvider,
5
+ } from '@shipfox/client-integrations';
6
+ import {describe, expect, test} from '@shipfox/vitest/vi';
7
+ import {deriveIntegrationReadiness} from './integration-readiness.js';
8
+
9
+ function provider(
10
+ key: string,
11
+ capabilities: IntegrationCapability[] = [],
12
+ displayName = key,
13
+ ): IntegrationProvider {
14
+ return {provider: key, displayName, capabilities};
15
+ }
16
+
17
+ function connection(
18
+ providerKey: string,
19
+ overrides: Partial<IntegrationConnection> = {},
20
+ ): IntegrationConnection {
21
+ return {
22
+ id: `connection-${providerKey}`,
23
+ workspaceId: 'workspace',
24
+ provider: providerKey,
25
+ externalAccountId: 'account',
26
+ slug: `${providerKey}_account`,
27
+ displayName: providerKey,
28
+ lifecycleStatus: 'active',
29
+ capabilities: [],
30
+ createdAt: '2026-01-01T00:00:00.000Z',
31
+ updatedAt: '2026-01-01T00:00:00.000Z',
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ const GITHUB = provider('github', ['source_control', 'agent_tools'], 'GitHub');
37
+ const LINEAR = provider('linear', ['agent_tools'], 'Linear');
38
+ const WEBHOOK = provider('webhook', [], 'Webhook');
39
+
40
+ describe('deriveIntegrationReadiness', () => {
41
+ test('reports a provider as connected when at least one connection is active', () => {
42
+ const readiness = deriveIntegrationReadiness({
43
+ providers: [LINEAR],
44
+ connections: [
45
+ connection('linear', {lifecycleStatus: 'active'}),
46
+ connection('linear', {id: 'other', lifecycleStatus: 'error'}),
47
+ ],
48
+ });
49
+
50
+ expect(readiness.providers).toEqual([
51
+ {
52
+ provider: 'linear',
53
+ displayName: 'Linear',
54
+ capabilities: ['agent_tools'],
55
+ connected: true,
56
+ attention: false,
57
+ },
58
+ ]);
59
+ });
60
+
61
+ test('reports a provider as needing attention when connections exist but none is active', () => {
62
+ const readiness = deriveIntegrationReadiness({
63
+ providers: [LINEAR],
64
+ connections: [
65
+ connection('linear', {lifecycleStatus: 'error'}),
66
+ connection('linear', {id: 'other', lifecycleStatus: 'disabled'}),
67
+ ],
68
+ });
69
+
70
+ expect(readiness.providers).toEqual([
71
+ {
72
+ provider: 'linear',
73
+ displayName: 'Linear',
74
+ capabilities: ['agent_tools'],
75
+ connected: false,
76
+ attention: true,
77
+ },
78
+ ]);
79
+ expect(readiness.attentionProviders).toEqual(['linear']);
80
+ });
81
+
82
+ test('reports a provider with no connections as neither connected nor in attention', () => {
83
+ const readiness = deriveIntegrationReadiness({
84
+ providers: [LINEAR, GITHUB],
85
+ connections: [connection('github', {lifecycleStatus: 'active'})],
86
+ });
87
+
88
+ expect(readiness.providers).toEqual([
89
+ {
90
+ provider: 'linear',
91
+ displayName: 'Linear',
92
+ capabilities: ['agent_tools'],
93
+ connected: false,
94
+ attention: false,
95
+ },
96
+ {
97
+ provider: 'github',
98
+ displayName: 'GitHub',
99
+ capabilities: ['source_control', 'agent_tools'],
100
+ connected: true,
101
+ attention: false,
102
+ },
103
+ ]);
104
+ });
105
+
106
+ test('ignores connections for providers outside the catalog', () => {
107
+ const readiness = deriveIntegrationReadiness({
108
+ providers: [LINEAR],
109
+ connections: [connection('unknown', {lifecycleStatus: 'active'})],
110
+ });
111
+
112
+ expect(readiness.providers).toEqual([
113
+ {
114
+ provider: 'linear',
115
+ displayName: 'Linear',
116
+ capabilities: ['agent_tools'],
117
+ connected: false,
118
+ attention: false,
119
+ },
120
+ ]);
121
+ expect(readiness.hasToolIntegration).toBe(false);
122
+ });
123
+
124
+ test('orders attention providers by the most recent connection update first', () => {
125
+ const readiness = deriveIntegrationReadiness({
126
+ providers: [GITHUB, LINEAR, WEBHOOK],
127
+ connections: [
128
+ connection('linear', {
129
+ lifecycleStatus: 'error',
130
+ updatedAt: '2026-03-01T00:00:00.000Z',
131
+ }),
132
+ connection('webhook', {
133
+ lifecycleStatus: 'error',
134
+ updatedAt: '2026-02-01T00:00:00.000Z',
135
+ }),
136
+ connection('github', {
137
+ lifecycleStatus: 'disabled',
138
+ updatedAt: '2026-01-01T00:00:00.000Z',
139
+ }),
140
+ ],
141
+ });
142
+
143
+ expect(readiness.attentionProviders).toEqual(['linear', 'webhook', 'github']);
144
+ });
145
+
146
+ test('uses the newest of several connections per provider for ordering', () => {
147
+ const readiness = deriveIntegrationReadiness({
148
+ providers: [LINEAR, WEBHOOK],
149
+ connections: [
150
+ connection('linear', {
151
+ id: 'stale',
152
+ lifecycleStatus: 'error',
153
+ updatedAt: '2026-01-01T00:00:00.000Z',
154
+ }),
155
+ connection('linear', {
156
+ id: 'fresh',
157
+ lifecycleStatus: 'disabled',
158
+ updatedAt: '2026-04-01T00:00:00.000Z',
159
+ }),
160
+ connection('webhook', {
161
+ lifecycleStatus: 'error',
162
+ updatedAt: '2026-03-01T00:00:00.000Z',
163
+ }),
164
+ ],
165
+ });
166
+
167
+ expect(readiness.attentionProviders).toEqual(['linear', 'webhook']);
168
+ });
169
+
170
+ test('keeps catalog order when attention providers updated at the same time', () => {
171
+ const readiness = deriveIntegrationReadiness({
172
+ providers: [GITHUB, LINEAR, WEBHOOK],
173
+ connections: [
174
+ connection('linear', {lifecycleStatus: 'error'}),
175
+ connection('webhook', {lifecycleStatus: 'error'}),
176
+ connection('github', {lifecycleStatus: 'error'}),
177
+ ],
178
+ });
179
+
180
+ expect(readiness.attentionProviders).toEqual(['github', 'linear', 'webhook']);
181
+ });
182
+
183
+ test('skips connections with an unparsable updated_at when ordering', () => {
184
+ const readiness = deriveIntegrationReadiness({
185
+ providers: [LINEAR, WEBHOOK],
186
+ connections: [
187
+ connection('linear', {lifecycleStatus: 'error', updatedAt: 'not-a-date'}),
188
+ connection('webhook', {lifecycleStatus: 'error', updatedAt: '2026-02-01T00:00:00.000Z'}),
189
+ ],
190
+ });
191
+
192
+ expect(readiness.attentionProviders).toEqual(['webhook', 'linear']);
193
+ });
194
+
195
+ test('reports no attention providers when no connection needs attention', () => {
196
+ const readiness = deriveIntegrationReadiness({
197
+ providers: [GITHUB, LINEAR],
198
+ connections: [
199
+ connection('github', {lifecycleStatus: 'active'}),
200
+ connection('linear', {lifecycleStatus: 'active'}),
201
+ ],
202
+ });
203
+
204
+ expect(readiness.attentionProviders).toEqual([]);
205
+ });
206
+
207
+ test('reports hasSourceControl only for an active source-control connection', () => {
208
+ expect(
209
+ deriveIntegrationReadiness({
210
+ providers: [GITHUB],
211
+ connections: [connection('github', {lifecycleStatus: 'active'})],
212
+ }).hasSourceControl,
213
+ ).toBe(true);
214
+
215
+ expect(
216
+ deriveIntegrationReadiness({
217
+ providers: [GITHUB],
218
+ connections: [connection('github', {lifecycleStatus: 'error'})],
219
+ }).hasSourceControl,
220
+ ).toBe(false);
221
+
222
+ expect(
223
+ deriveIntegrationReadiness({
224
+ providers: [LINEAR],
225
+ connections: [connection('linear', {lifecycleStatus: 'active'})],
226
+ }).hasSourceControl,
227
+ ).toBe(false);
228
+ });
229
+
230
+ test('reports hasToolIntegration for an active connection without source_control', () => {
231
+ expect(
232
+ deriveIntegrationReadiness({
233
+ providers: [LINEAR],
234
+ connections: [connection('linear', {lifecycleStatus: 'active'})],
235
+ }).hasToolIntegration,
236
+ ).toBe(true);
237
+
238
+ expect(
239
+ deriveIntegrationReadiness({
240
+ providers: [WEBHOOK],
241
+ connections: [connection('webhook', {lifecycleStatus: 'active'})],
242
+ }).hasToolIntegration,
243
+ ).toBe(true);
244
+ });
245
+
246
+ test('GitHub never satisfies hasToolIntegration', () => {
247
+ expect(
248
+ deriveIntegrationReadiness({
249
+ providers: [GITHUB],
250
+ connections: [connection('github', {lifecycleStatus: 'active'})],
251
+ }).hasToolIntegration,
252
+ ).toBe(false);
253
+ });
254
+
255
+ test('an inactive tool connection does not satisfy hasToolIntegration', () => {
256
+ expect(
257
+ deriveIntegrationReadiness({
258
+ providers: [LINEAR],
259
+ connections: [connection('linear', {lifecycleStatus: 'error'})],
260
+ }).hasToolIntegration,
261
+ ).toBe(false);
262
+ });
263
+
264
+ test('combines providers in one readiness report', () => {
265
+ const readiness = deriveIntegrationReadiness({
266
+ providers: [GITHUB, LINEAR, WEBHOOK],
267
+ connections: [
268
+ connection('github', {lifecycleStatus: 'active'}),
269
+ connection('linear', {lifecycleStatus: 'error'}),
270
+ ],
271
+ });
272
+
273
+ expect(readiness.hasSourceControl).toBe(true);
274
+ expect(readiness.hasToolIntegration).toBe(false);
275
+ expect(readiness.attentionProviders).toEqual(['linear']);
276
+ });
277
+ });
@@ -0,0 +1,93 @@
1
+ import {
2
+ type IntegrationCapability,
3
+ type IntegrationConnection,
4
+ type IntegrationProvider,
5
+ isUsableConnection,
6
+ } from '@shipfox/client-integrations';
7
+
8
+ /**
9
+ * Per-provider connection state for the workspace setup readiness model.
10
+ * `connected` and `attention` are mutually exclusive: a provider with at
11
+ * least one active connection is connected, a provider whose connections
12
+ * exist but are all inactive needs attention, and a provider without
13
+ * connections is neither.
14
+ */
15
+ export interface IntegrationProviderReadiness {
16
+ provider: string;
17
+ displayName: string;
18
+ capabilities: ReadonlyArray<IntegrationCapability>;
19
+ connected: boolean;
20
+ attention: boolean;
21
+ }
22
+
23
+ export interface WorkspaceIntegrationReadiness {
24
+ providers: ReadonlyArray<IntegrationProviderReadiness>;
25
+ /** Providers in attention, most recently updated connection first. */
26
+ attentionProviders: readonly string[];
27
+ /** At least one active connection to a source-control provider. */
28
+ hasSourceControl: boolean;
29
+ /** At least one active connection to a provider without `source_control`. */
30
+ hasToolIntegration: boolean;
31
+ }
32
+
33
+ export interface IntegrationReadinessInput {
34
+ providers: readonly IntegrationProvider[];
35
+ connections: readonly IntegrationConnection[];
36
+ }
37
+
38
+ /**
39
+ * Derives the workspace integration readiness from the provider catalog and
40
+ * the workspace's connections. The result is the shared input for the setup
41
+ * checklist and for the first-workflow spec's contextual "Connect X" prompt.
42
+ */
43
+ export function deriveIntegrationReadiness({
44
+ providers,
45
+ connections,
46
+ }: IntegrationReadinessInput): WorkspaceIntegrationReadiness {
47
+ const readiness = providers.map((provider) => {
48
+ const providerConnections = connections.filter(
49
+ (connection) => connection.provider === provider.provider,
50
+ );
51
+ const connected = providerConnections.some(isUsableConnection);
52
+ return {
53
+ provider: provider.provider,
54
+ displayName: provider.displayName,
55
+ capabilities: provider.capabilities,
56
+ connected,
57
+ attention: providerConnections.length > 0 && !connected,
58
+ };
59
+ });
60
+
61
+ const latestConnectionUpdate = latestConnectionUpdateByProvider(connections);
62
+
63
+ const attentionProviders = readiness
64
+ .filter((provider) => provider.attention)
65
+ .sort((a, b) => {
66
+ const aUpdate = latestConnectionUpdate.get(a.provider) ?? 0;
67
+ const bUpdate = latestConnectionUpdate.get(b.provider) ?? 0;
68
+ return bUpdate - aUpdate;
69
+ })
70
+ .map((provider) => provider.provider);
71
+
72
+ const hasSourceControl = readiness.some(
73
+ (provider) => provider.connected && provider.capabilities.includes('source_control'),
74
+ );
75
+ const hasToolIntegration = readiness.some(
76
+ (provider) => provider.connected && !provider.capabilities.includes('source_control'),
77
+ );
78
+
79
+ return {providers: readiness, attentionProviders, hasSourceControl, hasToolIntegration};
80
+ }
81
+
82
+ function latestConnectionUpdateByProvider(
83
+ connections: readonly IntegrationConnection[],
84
+ ): Map<string, number> {
85
+ const updates = new Map<string, number>();
86
+ for (const connection of connections) {
87
+ const timestamp = Date.parse(connection.updatedAt);
88
+ if (Number.isNaN(timestamp)) continue;
89
+ const current = updates.get(connection.provider) ?? 0;
90
+ if (timestamp > current) updates.set(connection.provider, timestamp);
91
+ }
92
+ return updates;
93
+ }