@cat-factory/app 0.36.0 → 0.37.1
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/app/components/auth/UserMenu.vue +11 -1
- package/app/components/brainstorm/BrainstormWindow.vue +2 -1
- package/app/components/clarity/ClarityReviewWindow.vue +2 -1
- package/app/components/layout/IntegrationBackTitle.vue +12 -7
- package/app/components/layout/IntegrationsHub.vue +191 -43
- package/app/components/layout/PersonalSetupModal.vue +141 -0
- package/app/components/pipeline/PipelineBuilder.vue +1 -1
- package/app/components/providers/VendorCredentialsModal.vue +7 -2
- package/app/composables/api/accounts.ts +36 -51
- package/app/composables/api/auth.ts +20 -19
- package/app/composables/api/board.ts +60 -40
- package/app/composables/api/bootstrap.ts +25 -22
- package/app/composables/api/client.ts +102 -0
- package/app/composables/api/context.ts +25 -6
- package/app/composables/api/documents.ts +36 -34
- package/app/composables/api/execution.ts +65 -48
- package/app/composables/api/followUps.ts +26 -26
- package/app/composables/api/fragments.ts +47 -34
- package/app/composables/api/github.ts +65 -45
- package/app/composables/api/humanReview.ts +7 -6
- package/app/composables/api/humanTest.ts +15 -11
- package/app/composables/api/kaizen.ts +8 -6
- package/app/composables/api/localSettings.ts +5 -4
- package/app/composables/api/models.ts +58 -51
- package/app/composables/api/notifications.ts +13 -7
- package/app/composables/api/presets.ts +34 -28
- package/app/composables/api/providerConnections.ts +68 -26
- package/app/composables/api/recurring.ts +40 -30
- package/app/composables/api/releaseHealth.ts +28 -26
- package/app/composables/api/reviews.ts +136 -114
- package/app/composables/api/sandbox.ts +52 -34
- package/app/composables/api/slack.ts +22 -25
- package/app/composables/api/spec.ts +3 -3
- package/app/composables/api/tasks.ts +42 -41
- package/app/composables/api/userSecrets.ts +12 -17
- package/app/composables/api/workspaces.ts +21 -15
- package/app/composables/useApi.ts +9 -1
- package/app/composables/useIntegrationBack.ts +9 -3
- package/app/composables/useSourceIntegration.ts +107 -0
- package/app/composables/useUpsertList.spec.ts +60 -0
- package/app/composables/useUpsertList.ts +57 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/auth.ts +2 -1
- package/app/stores/board.ts +2 -1
- package/app/stores/brainstorm.ts +2 -2
- package/app/stores/clarity.ts +6 -2
- package/app/stores/documents.ts +27 -62
- package/app/stores/execution.ts +3 -2
- package/app/stores/github.ts +1 -2
- package/app/stores/mergePresets.ts +2 -6
- package/app/stores/notifications.ts +9 -6
- package/app/stores/pipelines.ts +1 -1
- package/app/stores/recurringPipelines.ts +2 -7
- package/app/stores/sandbox.ts +1 -2
- package/app/stores/tasks.ts +25 -76
- package/app/stores/ui.ts +62 -19
- package/app/types/accountSettings.ts +11 -36
- package/app/types/accounts.ts +16 -71
- package/app/types/bootstrap.ts +13 -75
- package/app/types/brainstorm.ts +13 -38
- package/app/types/clarity.ts +12 -43
- package/app/types/consensus.ts +16 -89
- package/app/types/documents.ts +19 -94
- package/app/types/domain.ts +54 -586
- package/app/types/execution.ts +48 -515
- package/app/types/fragments.ts +15 -83
- package/app/types/github.ts +25 -161
- package/app/types/incidentEnrichment.ts +10 -25
- package/app/types/localModels.ts +11 -61
- package/app/types/localSettings.ts +9 -26
- package/app/types/merge.ts +10 -68
- package/app/types/model-presets.ts +7 -28
- package/app/types/models.ts +16 -164
- package/app/types/notifications.ts +18 -77
- package/app/types/openrouter.ts +8 -34
- package/app/types/providerConnections.ts +21 -41
- package/app/types/provisioningLogs.ts +9 -29
- package/app/types/recurring.ts +10 -63
- package/app/types/releaseHealth.ts +15 -39
- package/app/types/requirements.ts +14 -84
- package/app/types/sandbox.ts +45 -161
- package/app/types/services.ts +3 -22
- package/app/types/slack.ts +10 -47
- package/app/types/spec.ts +15 -68
- package/app/types/tasks.ts +15 -111
- package/app/types/tracker.ts +9 -24
- package/app/types/userSecrets.ts +12 -47
- package/package.json +9 -2
|
@@ -1,93 +1,78 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import {
|
|
2
|
+
addAccountMemberContract,
|
|
3
|
+
connectEmailContract,
|
|
4
|
+
createAccountContract,
|
|
5
|
+
createInvitationContract,
|
|
6
|
+
disconnectEmailContract,
|
|
7
|
+
getAccountSettingsContract,
|
|
8
|
+
getEmailConnectionContract,
|
|
9
|
+
listAccountMembersContract,
|
|
10
|
+
listAccountsContract,
|
|
11
|
+
listInvitationsContract,
|
|
12
|
+
revokeInvitationContract,
|
|
13
|
+
setMemberRolesContract,
|
|
14
|
+
testEmailContract,
|
|
15
|
+
updateAccountContract,
|
|
16
|
+
updateAccountSettingsContract,
|
|
17
|
+
} from '@cat-factory/contracts'
|
|
18
|
+
import type { AccountRole, UpdateAccountInput } from '~/types/domain'
|
|
19
|
+
import type { UpdateAccountSettingsInput } from '~/types/accountSettings'
|
|
11
20
|
import type { ApiContext } from './context'
|
|
12
21
|
|
|
13
22
|
/** Account (tenancy) management: orgs, members, invitations + the email sender. */
|
|
14
|
-
export function accountsApi({
|
|
23
|
+
export function accountsApi({ send }: ApiContext) {
|
|
15
24
|
return {
|
|
16
25
|
// ---- accounts (tenancy) -----------------------------------------------
|
|
17
26
|
// The accounts the user can switch between (personal + orgs), org creation
|
|
18
27
|
// and membership management. Empty when auth is disabled (dev).
|
|
19
|
-
listAccounts: () =>
|
|
28
|
+
listAccounts: () => send(listAccountsContract, {}),
|
|
20
29
|
|
|
21
30
|
createAccount: (body: { name: string; githubAccountLogin?: string }) =>
|
|
22
|
-
|
|
31
|
+
send(createAccountContract, { body }),
|
|
23
32
|
|
|
24
33
|
updateAccount: (accountId: string, body: UpdateAccountInput) =>
|
|
25
|
-
|
|
34
|
+
send(updateAccountContract, { pathParams: { accountId }, body }),
|
|
26
35
|
|
|
27
36
|
listAccountMembers: (accountId: string) =>
|
|
28
|
-
|
|
37
|
+
send(listAccountMembersContract, { pathParams: { accountId } }),
|
|
29
38
|
|
|
30
|
-
addAccountMember: (accountId: string, body:
|
|
31
|
-
|
|
32
|
-
method: 'POST',
|
|
33
|
-
body,
|
|
34
|
-
}),
|
|
39
|
+
addAccountMember: (accountId: string, body: { userId: string; roles?: AccountRole[] }) =>
|
|
40
|
+
send(addAccountMemberContract, { pathParams: { accountId }, body }),
|
|
35
41
|
|
|
36
42
|
setMemberRoles: (accountId: string, userId: string, roles: AccountRole[]) =>
|
|
37
|
-
|
|
38
|
-
`/accounts/${encodeURIComponent(accountId)}/members/${encodeURIComponent(userId)}/roles`,
|
|
39
|
-
{ method: 'PATCH', body: { roles } },
|
|
40
|
-
),
|
|
43
|
+
send(setMemberRolesContract, { pathParams: { accountId, userId }, body: { roles } }),
|
|
41
44
|
|
|
42
45
|
// Invitations: invite teammates by email into an org account.
|
|
43
46
|
listInvitations: (accountId: string) =>
|
|
44
|
-
|
|
47
|
+
send(listInvitationsContract, { pathParams: { accountId } }),
|
|
45
48
|
|
|
46
49
|
createInvitation: (accountId: string, body: { email: string; roles?: AccountRole[] }) =>
|
|
47
|
-
|
|
48
|
-
`/accounts/${encodeURIComponent(accountId)}/invitations`,
|
|
49
|
-
{ method: 'POST', body },
|
|
50
|
-
),
|
|
50
|
+
send(createInvitationContract, { pathParams: { accountId }, body }),
|
|
51
51
|
|
|
52
52
|
revokeInvitation: (accountId: string, invitationId: string) =>
|
|
53
|
-
|
|
54
|
-
`/accounts/${encodeURIComponent(accountId)}/invitations/${encodeURIComponent(invitationId)}`,
|
|
55
|
-
{ method: 'DELETE' },
|
|
56
|
-
),
|
|
53
|
+
send(revokeInvitationContract, { pathParams: { accountId, invitationId } }),
|
|
57
54
|
|
|
58
55
|
// Per-account email sender (UI-onboarded): connect/inspect/disconnect/test.
|
|
59
56
|
getEmailConnection: (accountId: string) =>
|
|
60
|
-
|
|
61
|
-
`/accounts/${encodeURIComponent(accountId)}/email-connection`,
|
|
62
|
-
),
|
|
57
|
+
send(getEmailConnectionContract, { pathParams: { accountId } }),
|
|
63
58
|
|
|
64
59
|
connectEmail: (
|
|
65
60
|
accountId: string,
|
|
66
61
|
body: { provider: 'sendgrid' | 'resend'; apiKey: string; fromAddress: string },
|
|
67
|
-
) =>
|
|
68
|
-
http<EmailConnection>(`/accounts/${encodeURIComponent(accountId)}/email-connection`, {
|
|
69
|
-
method: 'POST',
|
|
70
|
-
body,
|
|
71
|
-
}),
|
|
62
|
+
) => send(connectEmailContract, { pathParams: { accountId }, body }),
|
|
72
63
|
|
|
73
64
|
disconnectEmail: (accountId: string) =>
|
|
74
|
-
|
|
65
|
+
send(disconnectEmailContract, { pathParams: { accountId } }),
|
|
75
66
|
|
|
76
67
|
testEmail: (accountId: string, to: string) =>
|
|
77
|
-
|
|
78
|
-
method: 'POST',
|
|
79
|
-
body: { to },
|
|
80
|
-
}),
|
|
68
|
+
send(testEmailContract, { pathParams: { accountId }, body: { to } }),
|
|
81
69
|
|
|
82
70
|
// Per-account deployment settings (admin only): integration secrets (Slack OAuth +
|
|
83
71
|
// web-search keys), sealed at rest. Read returns config + non-secret summary only.
|
|
84
72
|
getAccountSettings: (accountId: string) =>
|
|
85
|
-
|
|
73
|
+
send(getAccountSettingsContract, { pathParams: { accountId } }),
|
|
86
74
|
|
|
87
75
|
updateAccountSettings: (accountId: string, body: UpdateAccountSettingsInput) =>
|
|
88
|
-
|
|
89
|
-
method: 'PUT',
|
|
90
|
-
body,
|
|
91
|
-
}),
|
|
76
|
+
send(updateAccountSettingsContract, { pathParams: { accountId }, body }),
|
|
92
77
|
}
|
|
93
78
|
}
|
|
@@ -1,42 +1,43 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
acceptInvitationContract,
|
|
3
|
+
authConfigContract,
|
|
4
|
+
logoutContract,
|
|
5
|
+
meContract,
|
|
6
|
+
passwordLoginContract,
|
|
7
|
+
peekInvitationContract,
|
|
8
|
+
signupContract,
|
|
9
|
+
} from '@cat-factory/contracts'
|
|
2
10
|
import type { ApiContext } from './context'
|
|
3
11
|
|
|
4
12
|
/** Auth/session endpoints + the events-WebSocket ticket mint. */
|
|
5
|
-
export function authApi({ http, ws }: ApiContext) {
|
|
13
|
+
export function authApi({ http, send, ws }: ApiContext) {
|
|
6
14
|
return {
|
|
7
15
|
// ---- auth -------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
providers?: { github: boolean; password: boolean; google: boolean }
|
|
12
|
-
/** Local-mode signals; present only when the backend is the local facade. */
|
|
13
|
-
localMode?: { enabled: boolean; githubPatSetupUrl?: string }
|
|
14
|
-
}>('/auth/config'),
|
|
16
|
+
// The `/auth/*` JSON endpoints are mounted under the `/auth` prefix; their
|
|
17
|
+
// contract paths are relative to it.
|
|
18
|
+
getAuthConfig: () => send(authConfigContract, { pathPrefix: '/auth' }),
|
|
15
19
|
|
|
16
|
-
getMe: () =>
|
|
20
|
+
getMe: () => send(meContract, { pathPrefix: '/auth' }),
|
|
17
21
|
|
|
18
22
|
signup: (body: { email: string; password: string; name?: string; invite?: string }) =>
|
|
19
|
-
|
|
23
|
+
send(signupContract, { pathPrefix: '/auth', body }),
|
|
20
24
|
|
|
21
25
|
passwordLogin: (body: { email: string; password: string }) =>
|
|
22
|
-
|
|
26
|
+
send(passwordLoginContract, { pathPrefix: '/auth', body }),
|
|
23
27
|
|
|
24
28
|
peekInvite: (token: string) =>
|
|
25
|
-
|
|
26
|
-
`/auth/invitations/${encodeURIComponent(token)}`,
|
|
27
|
-
),
|
|
29
|
+
send(peekInvitationContract, { pathPrefix: '/auth', pathParams: { token } }),
|
|
28
30
|
|
|
29
31
|
acceptInvite: (token: string) =>
|
|
30
|
-
|
|
31
|
-
method: 'POST',
|
|
32
|
-
}),
|
|
32
|
+
send(acceptInvitationContract, { pathPrefix: '/auth', pathParams: { token } }),
|
|
33
33
|
|
|
34
|
-
logout: () =>
|
|
34
|
+
logout: () => send(logoutContract, { pathPrefix: '/auth' }),
|
|
35
35
|
|
|
36
36
|
// Mint a short-lived, workspace-scoped ticket for the events WebSocket. A
|
|
37
37
|
// browser can't set Authorization on a WS handshake, so the socket auths from
|
|
38
38
|
// this `?ticket=` instead of the long-lived session token. Empty string when
|
|
39
39
|
// auth is disabled (dev) — the handshake is open in that case.
|
|
40
|
+
// No route contract exists for this endpoint, so it stays on the raw `http` client.
|
|
40
41
|
mintEventsTicket: (workspaceId: string) =>
|
|
41
42
|
http<{ ticket: string; expiresInMs?: number }>(`${ws(workspaceId)}/events/ticket`, {
|
|
42
43
|
method: 'POST',
|
|
@@ -1,34 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
addEpicContract,
|
|
3
|
+
addFrameContract,
|
|
4
|
+
addModuleContract,
|
|
5
|
+
addServiceFromRepoContract,
|
|
6
|
+
addTaskContract,
|
|
7
|
+
assignEpicContract,
|
|
8
|
+
clonePipelineContract,
|
|
9
|
+
createPipelineContract,
|
|
10
|
+
deletePipelineContract,
|
|
11
|
+
listPipelinesContract,
|
|
12
|
+
moveBlockContract,
|
|
13
|
+
organizePipelineContract,
|
|
14
|
+
removeBlockContract,
|
|
15
|
+
reparentBlockContract,
|
|
16
|
+
toggleDependencyContract,
|
|
17
|
+
updateBlockContract,
|
|
18
|
+
updatePipelineContract,
|
|
19
|
+
} from '@cat-factory/contracts'
|
|
20
|
+
import type {
|
|
21
|
+
CreatePipelineInput,
|
|
22
|
+
UpdateBlockInput,
|
|
23
|
+
UpdatePipelineInput,
|
|
24
|
+
} from '@cat-factory/contracts'
|
|
25
|
+
import type { BlockType, CreateTaskType, TaskTypeFields } from '~/types/domain'
|
|
3
26
|
import type { ApiContext, Position } from './context'
|
|
4
27
|
|
|
5
|
-
/**
|
|
6
|
-
* Create/update body for a pipeline. `name`+`agentKinds` required on create, all optional on
|
|
7
|
-
* update; the parallel arrays are aligned to `agentKinds` and persisted only when non-default.
|
|
8
|
-
*/
|
|
9
|
-
interface PipelineWriteBody {
|
|
10
|
-
name?: string
|
|
11
|
-
agentKinds?: string[]
|
|
12
|
-
gates?: boolean[]
|
|
13
|
-
thresholds?: (number | null)[]
|
|
14
|
-
enabled?: boolean[]
|
|
15
|
-
consensus?: (ConsensusStepConfig | null)[]
|
|
16
|
-
gating?: (StepGating | null)[]
|
|
17
|
-
labels?: string[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
28
|
/** Board structure: block (frame/module/task) mutations + the pipeline library. */
|
|
21
|
-
export function boardApi({
|
|
29
|
+
export function boardApi({ send, ws }: ApiContext) {
|
|
22
30
|
return {
|
|
23
31
|
// ---- blocks -----------------------------------------------------------
|
|
24
32
|
addFrame: (workspaceId: string, body: { type: BlockType; position: Position }) =>
|
|
25
|
-
|
|
33
|
+
send(addFrameContract, { pathPrefix: ws(workspaceId), body }),
|
|
26
34
|
|
|
27
35
|
// Import an existing GitHub repo as a service frame (no bootstrap run).
|
|
28
36
|
addServiceFromRepo: (
|
|
29
37
|
workspaceId: string,
|
|
30
38
|
body: { repoGithubId: number; position?: Position; directory?: string; isMonorepo?: boolean },
|
|
31
|
-
) =>
|
|
39
|
+
) => send(addServiceFromRepoContract, { pathPrefix: ws(workspaceId), body }),
|
|
32
40
|
|
|
33
41
|
addTask: (
|
|
34
42
|
workspaceId: string,
|
|
@@ -44,54 +52,65 @@ export function boardApi({ http, ws }: ApiContext) {
|
|
|
44
52
|
agentConfig?: Record<string, string>
|
|
45
53
|
technical?: boolean
|
|
46
54
|
},
|
|
47
|
-
) =>
|
|
55
|
+
) => send(addTaskContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
|
|
48
56
|
|
|
49
57
|
addModule: (
|
|
50
58
|
workspaceId: string,
|
|
51
59
|
blockId: string,
|
|
52
60
|
body: { name: string; position?: Position },
|
|
53
|
-
) =>
|
|
61
|
+
) => send(addModuleContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
|
|
54
62
|
|
|
55
63
|
// Create an epic grouping node (optionally placed under a service/module).
|
|
56
64
|
addEpic: (
|
|
57
65
|
workspaceId: string,
|
|
58
66
|
body: { title: string; description?: string; position: Position; parentId?: string },
|
|
59
|
-
) =>
|
|
67
|
+
) => send(addEpicContract, { pathPrefix: ws(workspaceId), body }),
|
|
60
68
|
|
|
61
69
|
// Assign a task to an epic, or detach it (epicId: null).
|
|
62
70
|
assignToEpic: (workspaceId: string, blockId: string, body: { epicId: string | null }) =>
|
|
63
|
-
|
|
71
|
+
send(assignEpicContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
|
|
64
72
|
|
|
65
|
-
updateBlock: (workspaceId: string, blockId: string, body:
|
|
66
|
-
|
|
73
|
+
updateBlock: (workspaceId: string, blockId: string, body: UpdateBlockInput) =>
|
|
74
|
+
send(updateBlockContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
|
|
67
75
|
|
|
68
76
|
moveBlock: (workspaceId: string, blockId: string, body: { position: Position }) =>
|
|
69
|
-
|
|
77
|
+
send(moveBlockContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
|
|
70
78
|
|
|
71
79
|
reparentBlock: (
|
|
72
80
|
workspaceId: string,
|
|
73
81
|
blockId: string,
|
|
74
82
|
body: { parentId: string; position: Position },
|
|
75
|
-
) =>
|
|
83
|
+
) =>
|
|
84
|
+
send(reparentBlockContract, { pathPrefix: ws(workspaceId), pathParams: { blockId }, body }),
|
|
76
85
|
|
|
77
86
|
removeBlock: (workspaceId: string, blockId: string) =>
|
|
78
|
-
|
|
87
|
+
send(removeBlockContract, { pathPrefix: ws(workspaceId), pathParams: { blockId } }),
|
|
79
88
|
|
|
80
89
|
toggleDependency: (workspaceId: string, blockId: string, body: { sourceId: string }) =>
|
|
81
|
-
|
|
90
|
+
send(toggleDependencyContract, {
|
|
91
|
+
pathPrefix: ws(workspaceId),
|
|
92
|
+
pathParams: { blockId },
|
|
93
|
+
body,
|
|
94
|
+
}),
|
|
82
95
|
|
|
83
96
|
// ---- pipelines --------------------------------------------------------
|
|
84
|
-
listPipelines: (workspaceId: string) =>
|
|
97
|
+
listPipelines: (workspaceId: string) =>
|
|
98
|
+
send(listPipelinesContract, { pathPrefix: ws(workspaceId) }),
|
|
85
99
|
|
|
86
|
-
createPipeline: (workspaceId: string, body:
|
|
87
|
-
|
|
100
|
+
createPipeline: (workspaceId: string, body: CreatePipelineInput) =>
|
|
101
|
+
send(createPipelineContract, { pathPrefix: ws(workspaceId), body }),
|
|
88
102
|
|
|
89
|
-
updatePipeline: (workspaceId: string, pipelineId: string, body:
|
|
90
|
-
|
|
103
|
+
updatePipeline: (workspaceId: string, pipelineId: string, body: UpdatePipelineInput) =>
|
|
104
|
+
send(updatePipelineContract, {
|
|
105
|
+
pathPrefix: ws(workspaceId),
|
|
106
|
+
pathParams: { pipelineId },
|
|
107
|
+
body,
|
|
108
|
+
}),
|
|
91
109
|
|
|
92
110
|
clonePipeline: (workspaceId: string, pipelineId: string, body: { name?: string } = {}) =>
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
send(clonePipelineContract, {
|
|
112
|
+
pathPrefix: ws(workspaceId),
|
|
113
|
+
pathParams: { pipelineId },
|
|
95
114
|
body,
|
|
96
115
|
}),
|
|
97
116
|
|
|
@@ -102,12 +121,13 @@ export function boardApi({ http, ws }: ApiContext) {
|
|
|
102
121
|
pipelineId: string,
|
|
103
122
|
body: { labels?: string[]; archived?: boolean },
|
|
104
123
|
) =>
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
send(organizePipelineContract, {
|
|
125
|
+
pathPrefix: ws(workspaceId),
|
|
126
|
+
pathParams: { pipelineId },
|
|
107
127
|
body,
|
|
108
128
|
}),
|
|
109
129
|
|
|
110
130
|
removePipeline: (workspaceId: string, pipelineId: string) =>
|
|
111
|
-
|
|
131
|
+
send(deletePipelineContract, { pathPrefix: ws(workspaceId), pathParams: { pipelineId } }),
|
|
112
132
|
}
|
|
113
133
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createReferenceArchitectureContract,
|
|
3
|
+
deleteReferenceArchitectureContract,
|
|
4
|
+
listReferenceArchitecturesContract,
|
|
5
|
+
retryAgentRunContract,
|
|
6
|
+
startBootstrapJobContract,
|
|
7
|
+
stopAgentRunContract,
|
|
8
|
+
updateReferenceArchitectureContract,
|
|
9
|
+
} from '@cat-factory/contracts'
|
|
1
10
|
import type {
|
|
2
|
-
AgentRunKind,
|
|
3
|
-
BootstrapJob,
|
|
4
11
|
BootstrapRepoInput,
|
|
5
12
|
CreateReferenceArchitectureInput,
|
|
6
|
-
ExecutionInstance,
|
|
7
|
-
ReferenceArchitecture,
|
|
8
13
|
UpdateReferenceArchitectureInput,
|
|
9
14
|
} from '~/types/domain'
|
|
10
15
|
import type { ApiContext } from './context'
|
|
@@ -13,50 +18,48 @@ import type { ApiContext } from './context'
|
|
|
13
18
|
* Repo bootstrap (reference architectures + bootstrap jobs) and the unified
|
|
14
19
|
* agent-run failure/retry/stop surface shared by bootstrap + execution runs.
|
|
15
20
|
*/
|
|
16
|
-
export function bootstrapApi({
|
|
21
|
+
export function bootstrapApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
|
|
17
22
|
return {
|
|
18
23
|
// ---- repo bootstrap ---------------------------------------------------
|
|
19
24
|
listReferenceArchitectures: (workspaceId: string) =>
|
|
20
|
-
|
|
25
|
+
send(listReferenceArchitecturesContract, { pathPrefix: ws(workspaceId) }),
|
|
21
26
|
|
|
22
27
|
createReferenceArchitecture: (workspaceId: string, body: CreateReferenceArchitectureInput) =>
|
|
23
|
-
|
|
24
|
-
method: 'POST',
|
|
25
|
-
body,
|
|
26
|
-
}),
|
|
28
|
+
send(createReferenceArchitectureContract, { pathPrefix: ws(workspaceId), body }),
|
|
27
29
|
|
|
28
30
|
updateReferenceArchitecture: (
|
|
29
31
|
workspaceId: string,
|
|
30
32
|
id: string,
|
|
31
33
|
body: UpdateReferenceArchitectureInput,
|
|
32
34
|
) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
send(updateReferenceArchitectureContract, {
|
|
36
|
+
pathPrefix: ws(workspaceId),
|
|
37
|
+
pathParams: { id },
|
|
35
38
|
body,
|
|
36
39
|
}),
|
|
37
40
|
|
|
38
41
|
deleteReferenceArchitecture: (workspaceId: string, id: string) =>
|
|
39
|
-
|
|
42
|
+
send(deleteReferenceArchitectureContract, {
|
|
43
|
+
pathPrefix: ws(workspaceId),
|
|
44
|
+
pathParams: { id },
|
|
45
|
+
}),
|
|
40
46
|
|
|
41
47
|
bootstrapRepo: (workspaceId: string, body: BootstrapRepoInput) =>
|
|
42
|
-
|
|
48
|
+
send(startBootstrapJobContract, { pathPrefix: ws(workspaceId), body }),
|
|
43
49
|
|
|
44
50
|
// ---- agent runs (unified failure + retry) -----------------------------
|
|
45
51
|
// Retry any failed run (bootstrap or execution); the backend resolves the
|
|
46
52
|
// kind from the unified `agent_runs` table and re-drives the right flow.
|
|
47
53
|
retryAgentRun: (workspaceId: string, runId: string, password?: string) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
),
|
|
54
|
+
sendWith(pwHeaders(password), retryAgentRunContract, {
|
|
55
|
+
pathPrefix: ws(workspaceId),
|
|
56
|
+
pathParams: { id: runId },
|
|
57
|
+
}),
|
|
52
58
|
|
|
53
59
|
// Explicitly stop a running run (bootstrap or execution): the backend kills the
|
|
54
60
|
// per-run container and tears down the durable driver, then marks the run
|
|
55
61
|
// terminally cancelled so the board stops showing it as running.
|
|
56
62
|
stopAgentRun: (workspaceId: string, runId: string) =>
|
|
57
|
-
|
|
58
|
-
`${ws(workspaceId)}/agent-runs/${encodeURIComponent(runId)}/stop`,
|
|
59
|
-
{ method: 'POST' },
|
|
60
|
-
),
|
|
63
|
+
send(stopAgentRunContract, { pathPrefix: ws(workspaceId), pathParams: { id: runId } }),
|
|
61
64
|
}
|
|
62
65
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiContract,
|
|
3
|
+
ClientRequestParams,
|
|
4
|
+
DefaultStreaming,
|
|
5
|
+
InferNonSseClientResponse,
|
|
6
|
+
SuccessfulHttpStatusCode,
|
|
7
|
+
} from '@toad-contracts/core'
|
|
8
|
+
import {
|
|
9
|
+
type ContractRequestOptions,
|
|
10
|
+
sendByApiContract,
|
|
11
|
+
type WretchInstance,
|
|
12
|
+
} from '@toad-contracts/frontend-http-client'
|
|
13
|
+
import wretch from 'wretch'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The validated success-response body inferred from a route contract (every REST
|
|
17
|
+
* endpoint here is non-SSE). This is what {@link ApiSend} resolves to.
|
|
18
|
+
*/
|
|
19
|
+
export type SuccessBodyOf<T extends ApiContract> = Extract<
|
|
20
|
+
InferNonSseClientResponse<T>,
|
|
21
|
+
{ statusCode: SuccessfulHttpStatusCode }
|
|
22
|
+
>['body']
|
|
23
|
+
|
|
24
|
+
/** The request params a contract requires (pathParams/body/queryParams/headers), per the contract. */
|
|
25
|
+
export type SendParams<T extends ApiContract> = ClientRequestParams<
|
|
26
|
+
T,
|
|
27
|
+
DefaultStreaming<T['responsesByStatusCode']>
|
|
28
|
+
> &
|
|
29
|
+
ContractRequestOptions<true>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A bound, throw-on-error sender: validates the response against the contract and
|
|
33
|
+
* returns the success body, or throws the typed error (an `UnexpectedResponseError`
|
|
34
|
+
* or a declared non-2xx response). Preserves the throwing ergonomics the Pinia
|
|
35
|
+
* stores already expect from the old `$fetch` client.
|
|
36
|
+
*/
|
|
37
|
+
export type ApiSend = <T extends ApiContract>(
|
|
38
|
+
contract: T,
|
|
39
|
+
params: SendParams<T>,
|
|
40
|
+
) => Promise<SuccessBodyOf<T>>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the authed wretch client. Ports the concerns the old `$fetch` client had:
|
|
44
|
+
* base URL from runtime config, a lazily-read bearer token (so a fresh token applies
|
|
45
|
+
* without rebuilding the client), and a 401 → re-gate.
|
|
46
|
+
*/
|
|
47
|
+
export function createApiClient(): WretchInstance {
|
|
48
|
+
const apiBase = useRuntimeConfig().public.apiBase
|
|
49
|
+
return wretch(apiBase).middlewares([
|
|
50
|
+
(next) => async (url, opts) => {
|
|
51
|
+
const token = useAuthStore().token
|
|
52
|
+
if (token) {
|
|
53
|
+
opts.headers = {
|
|
54
|
+
...(opts.headers as Record<string, string> | undefined),
|
|
55
|
+
Authorization: `Bearer ${token}`,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const response = await next(url, opts)
|
|
59
|
+
// A 401 means our token lapsed or was revoked — drop it so the UI re-gates.
|
|
60
|
+
if (response.status === 401) useAuthStore().handleUnauthorized()
|
|
61
|
+
return response
|
|
62
|
+
},
|
|
63
|
+
])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send a contract request and unwrap to the success body (or throw the typed error).
|
|
68
|
+
* The public signature preserves per-contract inference for callers; inside,
|
|
69
|
+
* sendByApiContract's deeply-conditional result type can't be proven equal to
|
|
70
|
+
* SuccessBodyOf<T> generically, so the success body is asserted at this single boundary.
|
|
71
|
+
*/
|
|
72
|
+
export async function sendContract<T extends ApiContract>(
|
|
73
|
+
client: WretchInstance,
|
|
74
|
+
contract: T,
|
|
75
|
+
params: SendParams<T>,
|
|
76
|
+
): Promise<SuccessBodyOf<T>> {
|
|
77
|
+
const outcome = await sendByApiContract(client, contract, params)
|
|
78
|
+
if (outcome.error) throw outcome.error
|
|
79
|
+
return outcome.result!.body as SuccessBodyOf<T>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Curry {@link sendContract} over a client into the throw-on-error {@link ApiSend}. */
|
|
83
|
+
export function createSend(client: WretchInstance): ApiSend {
|
|
84
|
+
return (contract, params) => sendContract(client, contract, params)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Like {@link ApiSend} but augments the request with ambient headers not modelled by the
|
|
89
|
+
* contract (the personal-subscription unlock password, mirroring how the bearer token
|
|
90
|
+
* rides outside the wire body). `undefined` headers send unchanged.
|
|
91
|
+
*/
|
|
92
|
+
export type ApiSendWith = <T extends ApiContract>(
|
|
93
|
+
extraHeaders: Record<string, string> | undefined,
|
|
94
|
+
contract: T,
|
|
95
|
+
params: SendParams<T>,
|
|
96
|
+
) => Promise<SuccessBodyOf<T>>
|
|
97
|
+
|
|
98
|
+
/** Curry {@link sendContract} with per-call ambient headers (see {@link ApiSendWith}). */
|
|
99
|
+
export function createSendWith(client: WretchInstance): ApiSendWith {
|
|
100
|
+
return (extraHeaders, contract, params) =>
|
|
101
|
+
sendContract(extraHeaders ? client.headers(extraHeaders) : client, contract, params)
|
|
102
|
+
}
|
|
@@ -1,19 +1,38 @@
|
|
|
1
|
+
import type { WretchInstance } from '@toad-contracts/frontend-http-client'
|
|
1
2
|
import type { FragmentOwnerKind } from '~/types/domain'
|
|
3
|
+
import type { ApiSend, ApiSendWith } from './client'
|
|
2
4
|
|
|
3
5
|
/** The authed `$fetch` instance type — Nuxt's augmented client, as returned by `$fetch.create`. */
|
|
4
6
|
export type ApiHttp = ReturnType<typeof $fetch.create>
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Shared plumbing handed to every grouped API module. `useApi()` builds one of
|
|
8
|
-
* these (the authed
|
|
9
|
-
* each `*Api(ctx)` factory; the factories return the
|
|
10
|
-
* `useApi()` spreads into its single flat client object.
|
|
11
|
-
* this way keeps call sites unchanged
|
|
12
|
-
* ~100 endpoints live in cohesive
|
|
10
|
+
* these (the authed wretch client, the contract `send` helper + the path/header
|
|
11
|
+
* helpers) and passes it to each `*Api(ctx)` factory; the factories return the
|
|
12
|
+
* endpoint methods that `useApi()` spreads into its single flat client object.
|
|
13
|
+
* Splitting the client this way keeps call sites unchanged
|
|
14
|
+
* (`useApi().someMethod(...)`) while the ~100 endpoints live in cohesive
|
|
15
|
+
* per-domain files.
|
|
13
16
|
*/
|
|
14
17
|
export interface ApiContext {
|
|
15
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The authed `$fetch` instance (bearer token + 401 handling pre-wired).
|
|
20
|
+
* Transitional: API groups not yet migrated to contract `send` still use it.
|
|
21
|
+
*/
|
|
16
22
|
http: ApiHttp
|
|
23
|
+
/** The authed wretch client (bearer token + 401 handling pre-wired). */
|
|
24
|
+
client: WretchInstance
|
|
25
|
+
/**
|
|
26
|
+
* Contract sender: validates the response against the route contract and returns
|
|
27
|
+
* the success body, or throws the typed error. The single source of truth for
|
|
28
|
+
* path + method + request + response is the contract in `@cat-factory/contracts`.
|
|
29
|
+
*/
|
|
30
|
+
send: ApiSend
|
|
31
|
+
/**
|
|
32
|
+
* Like {@link send} but attaches ambient headers the contract doesn't model — today the
|
|
33
|
+
* personal-subscription unlock password on gated run calls (individual-usage vendors).
|
|
34
|
+
*/
|
|
35
|
+
sendWith: ApiSendWith
|
|
17
36
|
/** `/workspaces/:id` path prefix (id encoded). */
|
|
18
37
|
ws: (workspaceId: string) => string
|
|
19
38
|
/** Prompt-fragment library prefix, resolved from the owner scope (ADR 0006 §8). */
|