@cat-factory/app 0.35.0 → 0.37.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/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/gates/GateResultView.vue +107 -12
- package/app/components/layout/IntegrationBackTitle.vue +12 -7
- package/app/components/layout/IntegrationsHub.vue +191 -43
- package/app/components/layout/NotificationsInbox.vue +16 -0
- 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/components/slack/SlackPanel.vue +1 -0
- 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 +19 -0
- 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 +11 -1
- package/app/composables/useIntegrationBack.ts +9 -3
- 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/execution.ts +3 -2
- package/app/stores/github.ts +1 -2
- package/app/stores/humanReview.ts +41 -0
- package/app/stores/mergePresets.ts +2 -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/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 -582
- package/app/types/execution.ts +48 -499
- 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 -76
- 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/app/utils/catalog.ts +12 -0
- package/package.json +9 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// "My setup": the user-scoped sibling of the Integrations hub. It lists the connections
|
|
3
|
+
// that belong to the SIGNED-IN USER rather than the workspace — a personal GitHub token,
|
|
4
|
+
// own-machine local model runners, and individual-usage (personal) subscriptions — which
|
|
5
|
+
// previously sat, confusingly, among the workspace-wide integrations. Each row reuses the
|
|
6
|
+
// existing per-panel handlers via `ui.openFromPersonal(...)`, so opening one closes this
|
|
7
|
+
// hub, reveals that panel, and gives it a "Back to My setup" control (IntegrationBackTitle).
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
const userSecrets = useUserSecretsStore()
|
|
10
|
+
const localModels = useLocalModelsStore()
|
|
11
|
+
const personalSubs = usePersonalSubscriptionsStore()
|
|
12
|
+
|
|
13
|
+
// Load the cheap user-scoped status whenever the hub opens, so each row's badge is accurate.
|
|
14
|
+
watch(
|
|
15
|
+
() => ui.personalSetupOpen,
|
|
16
|
+
(isOpen) => {
|
|
17
|
+
if (!isOpen) return
|
|
18
|
+
void userSecrets.load().catch(() => {})
|
|
19
|
+
void localModels.load().catch(() => {})
|
|
20
|
+
void personalSubs.load().catch(() => {})
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const open = computed({
|
|
25
|
+
get: () => ui.personalSetupOpen,
|
|
26
|
+
set: (v: boolean) => (v ? ui.openPersonalSetup() : ui.closePersonalSetup()),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// One row. `connected` drives the badge; `status` is the connected-state line (a count or
|
|
30
|
+
// "Connected"). Mirrors the Integrations hub's row shape so the two hubs look identical.
|
|
31
|
+
interface PersonalItem {
|
|
32
|
+
key: string
|
|
33
|
+
icon: string
|
|
34
|
+
label: string
|
|
35
|
+
description: string
|
|
36
|
+
status?: string
|
|
37
|
+
connected: boolean
|
|
38
|
+
onClick: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PersonalGroup {
|
|
42
|
+
title: string
|
|
43
|
+
items: PersonalItem[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Open a user-scoped panel from this hub (sets the "came from My setup" marker).
|
|
47
|
+
function go(fn: () => void) {
|
|
48
|
+
ui.openFromPersonal(fn)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const groups = computed<PersonalGroup[]>(() => {
|
|
52
|
+
const out: PersonalGroup[] = []
|
|
53
|
+
|
|
54
|
+
// --- Source control --------------------------------------------------------
|
|
55
|
+
const pat = !!userSecrets.statusFor('github_pat')
|
|
56
|
+
out.push({
|
|
57
|
+
title: 'Source control',
|
|
58
|
+
items: [
|
|
59
|
+
{
|
|
60
|
+
key: 'github-pat',
|
|
61
|
+
icon: 'i-lucide-key-round',
|
|
62
|
+
label: 'My GitHub token',
|
|
63
|
+
description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
|
|
64
|
+
status: pat ? 'Connected' : undefined,
|
|
65
|
+
connected: pat,
|
|
66
|
+
onClick: () => go(ui.openUserSecrets),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// --- Models ----------------------------------------------------------------
|
|
72
|
+
const runnerCount = localModels.endpoints.length
|
|
73
|
+
const subCount = personalSubs.subscriptions.length
|
|
74
|
+
out.push({
|
|
75
|
+
title: 'Models',
|
|
76
|
+
items: [
|
|
77
|
+
{
|
|
78
|
+
key: 'local-runners',
|
|
79
|
+
icon: 'i-lucide-server',
|
|
80
|
+
label: 'My local runners',
|
|
81
|
+
description: 'Your own-machine model runners (Ollama, LM Studio, vLLM…).',
|
|
82
|
+
status: runnerCount ? `${runnerCount} connected` : undefined,
|
|
83
|
+
connected: runnerCount > 0,
|
|
84
|
+
onClick: () => go(ui.openLocalModels),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'personal-subs',
|
|
88
|
+
icon: 'i-lucide-user',
|
|
89
|
+
label: 'My subscriptions',
|
|
90
|
+
description:
|
|
91
|
+
'Individual-usage plans you unlock with a personal password (Claude, GLM, Codex).',
|
|
92
|
+
status: subCount ? `${subCount} connected` : undefined,
|
|
93
|
+
connected: subCount > 0,
|
|
94
|
+
onClick: () => go(() => ui.openVendorCredentials('personal')),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return out
|
|
100
|
+
})
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<template>
|
|
104
|
+
<UModal v-model:open="open" title="My setup" :ui="{ content: 'max-w-xl' }">
|
|
105
|
+
<template #body>
|
|
106
|
+
<div class="space-y-5">
|
|
107
|
+
<p class="text-xs text-slate-400">
|
|
108
|
+
Your personal connections — used for runs you start, and visible only to you.
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
<section v-for="group in groups" :key="group.title">
|
|
112
|
+
<h3 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
113
|
+
{{ group.title }}
|
|
114
|
+
</h3>
|
|
115
|
+
<div class="space-y-1.5">
|
|
116
|
+
<button
|
|
117
|
+
v-for="item in group.items"
|
|
118
|
+
:key="item.key"
|
|
119
|
+
type="button"
|
|
120
|
+
class="flex w-full items-center gap-3 rounded-lg border border-slate-800 bg-slate-900/50 px-3 py-2.5 text-left transition hover:border-slate-700 hover:bg-slate-900"
|
|
121
|
+
@click="item.onClick()"
|
|
122
|
+
>
|
|
123
|
+
<UIcon :name="item.icon" class="h-5 w-5 shrink-0 text-slate-300" />
|
|
124
|
+
<div class="min-w-0 flex-1">
|
|
125
|
+
<div class="flex items-center gap-2">
|
|
126
|
+
<span class="truncate text-sm font-medium text-slate-100">{{ item.label }}</span>
|
|
127
|
+
<UBadge v-if="item.connected" color="success" variant="subtle" size="sm">
|
|
128
|
+
{{ item.status || 'Connected' }}
|
|
129
|
+
</UBadge>
|
|
130
|
+
<span v-else class="text-[11px] text-slate-500">Not connected</span>
|
|
131
|
+
</div>
|
|
132
|
+
<p class="truncate text-xs text-slate-400">{{ item.description }}</p>
|
|
133
|
+
</div>
|
|
134
|
+
<UIcon name="i-lucide-chevron-right" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
</div>
|
|
139
|
+
</template>
|
|
140
|
+
</UModal>
|
|
141
|
+
</template>
|
|
@@ -31,7 +31,7 @@ function toggleGating(i: number) {
|
|
|
31
31
|
if (!cfg) return
|
|
32
32
|
cfg.gating = cfg.gating?.enabled
|
|
33
33
|
? { ...cfg.gating, enabled: false }
|
|
34
|
-
: { enabled: true, minRisk: 0.6, minImpact: 0.6 }
|
|
34
|
+
: { enabled: true, minRisk: 0.6, minImpact: 0.6, onMissingEstimate: 'consensus' }
|
|
35
35
|
}
|
|
36
36
|
const agents = useAgentsStore()
|
|
37
37
|
const ui = useUiStore()
|
|
@@ -22,7 +22,9 @@ const back = useIntegrationBack(open)
|
|
|
22
22
|
|
|
23
23
|
// Horizontal tabs replace the old long vertical scroll: each credential kind is its own
|
|
24
24
|
// section (pooled subscriptions, direct vendor keys, proxy/gateway keys, personal subs).
|
|
25
|
-
|
|
25
|
+
// Initialised from the ui store so a caller can deep-link to a tab — the user-scoped
|
|
26
|
+
// "My subscriptions" entry opens straight onto the `personal` tab.
|
|
27
|
+
const activeTab = ref(ui.vendorCredentialsTab)
|
|
26
28
|
const tabs = [
|
|
27
29
|
{ value: 'pool', label: 'Workspace pool', icon: 'i-lucide-users', slot: 'pool' },
|
|
28
30
|
{ value: 'direct', label: 'Direct providers', icon: 'i-lucide-key-round', slot: 'direct' },
|
|
@@ -51,7 +53,10 @@ const token = ref('')
|
|
|
51
53
|
const busy = ref(false)
|
|
52
54
|
|
|
53
55
|
watch(open, (isOpen) => {
|
|
54
|
-
if (isOpen
|
|
56
|
+
if (!isOpen) return
|
|
57
|
+
// Honour a deep-linked tab each time the modal opens (e.g. "My subscriptions" → personal).
|
|
58
|
+
activeTab.value = ui.vendorCredentialsTab
|
|
59
|
+
if (workspace.workspaceId) void creds.load(workspace.workspaceId)
|
|
55
60
|
})
|
|
56
61
|
|
|
57
62
|
/** Step-by-step instructions for the selected vendor. */
|
|
@@ -45,6 +45,7 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
45
45
|
// In-app only (not in ROUTABLE), but the map is exhaustive over the type.
|
|
46
46
|
decision_required: { enabled: false, channel: '' },
|
|
47
47
|
human_test_ready: { enabled: false, channel: '' },
|
|
48
|
+
human_review: { enabled: false, channel: '' },
|
|
48
49
|
followup_pending: { enabled: false, channel: '' },
|
|
49
50
|
})
|
|
50
51
|
const mentionsEnabled = ref(false)
|
|
@@ -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
|
}
|