@cat-factory/app 0.52.0 → 0.53.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.
|
@@ -88,6 +88,7 @@ const canSave = computed(() => {
|
|
|
88
88
|
function buildManifestPayload(): {
|
|
89
89
|
manifest: Record<string, unknown>
|
|
90
90
|
secrets: Record<string, string>
|
|
91
|
+
backendKind: string
|
|
91
92
|
} | null {
|
|
92
93
|
const template = descriptor.value?.manifestTemplate
|
|
93
94
|
if (!template) return null
|
|
@@ -106,7 +107,9 @@ function buildManifestPayload(): {
|
|
|
106
107
|
else providerConfig[f.key] = val
|
|
107
108
|
}
|
|
108
109
|
if (Object.keys(providerConfig).length) manifest.providerConfig = providerConfig
|
|
109
|
-
|
|
110
|
+
// Carry the selected kind so a CUSTOM backend's flat-form save is tagged with its slug
|
|
111
|
+
// (not silently wrapped into the built-in `manifest` backend).
|
|
112
|
+
return { manifest, secrets, backendKind: backendKind.value }
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
function notifyError(title: string, e: unknown) {
|
|
@@ -156,6 +159,8 @@ async function saveNative() {
|
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
// --- Manifest-editor actions (emitted from ProviderManifestEditor) ------------------
|
|
162
|
+
// Tag the raw-manifest save/test with the selected backend kind too, so a CUSTOM kind that
|
|
163
|
+
// ships no flat-form template (and thus uses the raw editor) isn't mis-tagged as `manifest`.
|
|
159
164
|
async function testManifest(payload: {
|
|
160
165
|
manifest: Record<string, unknown>
|
|
161
166
|
secrets: Record<string, string>
|
|
@@ -163,7 +168,7 @@ async function testManifest(payload: {
|
|
|
163
168
|
testing.value = true
|
|
164
169
|
testResult.value = null
|
|
165
170
|
try {
|
|
166
|
-
testResult.value = await store.test(props.kind, payload)
|
|
171
|
+
testResult.value = await store.test(props.kind, { ...payload, backendKind: backendKind.value })
|
|
167
172
|
} catch (e) {
|
|
168
173
|
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
169
174
|
} finally {
|
|
@@ -177,7 +182,7 @@ async function saveManifest(payload: {
|
|
|
177
182
|
}) {
|
|
178
183
|
busy.value = true
|
|
179
184
|
try {
|
|
180
|
-
await store.register(props.kind, payload)
|
|
185
|
+
await store.register(props.kind, { ...payload, backendKind: backendKind.value })
|
|
181
186
|
toastSaved()
|
|
182
187
|
} catch (e) {
|
|
183
188
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -187,14 +192,12 @@ async function saveManifest(payload: {
|
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
// --- Backend selector -----------------------------------------------------------------
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const backendKind = ref<BackendKind>('manifest')
|
|
197
|
-
const showBackendSelector = computed(() => true)
|
|
195
|
+
// Each infrastructure tab configures one of the backend KINDS registered for its subsystem:
|
|
196
|
+
// the built-in BYO `manifest` backend, the native `kubernetes` backend, or any CUSTOM kind a
|
|
197
|
+
// deployment registered programmatically. The list is snapshot-driven (built-in fallback in
|
|
198
|
+
// the store until it loads); the two K8s backends have bespoke forms, every other kind
|
|
199
|
+
// (manifest + custom) uses the descriptor-driven flat form. Defaults to the saved kind.
|
|
200
|
+
const backendKind = ref<string>('manifest')
|
|
198
201
|
const backendSelectorLabel = computed(() =>
|
|
199
202
|
t(
|
|
200
203
|
props.kind === 'environment'
|
|
@@ -202,25 +205,40 @@ const backendSelectorLabel = computed(() =>
|
|
|
202
205
|
: 'settings.providerConnection.backend.selectorLabel',
|
|
203
206
|
),
|
|
204
207
|
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return t(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// Built-in kinds keep their localized labels; a custom kind shows its snapshot displayLabel.
|
|
209
|
+
function backendKindLabel(option: { kind: string; label: string }): string {
|
|
210
|
+
if (option.kind === 'kubernetes') return t('settings.providerConnection.backend.kubernetes')
|
|
211
|
+
if (option.kind === 'manifest') {
|
|
212
|
+
return t(
|
|
213
|
+
props.kind === 'environment'
|
|
214
|
+
? 'settings.providerConnection.backend.environmentManifest'
|
|
215
|
+
: 'settings.providerConnection.backend.manifest',
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
return option.label
|
|
212
219
|
}
|
|
213
220
|
const backendKindItems = computed(() =>
|
|
214
|
-
|
|
221
|
+
store.backendKindsFor(props.kind).map((o) => ({ label: backendKindLabel(o), value: o.kind })),
|
|
215
222
|
)
|
|
216
223
|
watch(
|
|
217
224
|
() => connection.value,
|
|
218
225
|
(c) => {
|
|
219
|
-
if (c?.kind
|
|
226
|
+
if (c?.kind) backendKind.value = c.kind
|
|
220
227
|
},
|
|
221
228
|
{ immediate: true },
|
|
222
229
|
)
|
|
223
230
|
|
|
231
|
+
// Switching the backend kind re-probes ONLY that kind's descriptor (so a not-yet-connected
|
|
232
|
+
// custom kind's connect form renders). Always pass the explicit kind — including `manifest`,
|
|
233
|
+
// so picking it describes the manifest backend rather than falling back to the stored kind —
|
|
234
|
+
// and use `loadDescriptor` (not `loadKind`) so the stored connection isn't re-fetched and the
|
|
235
|
+
// selector isn't bounced back to the stored kind by the `connection` watch.
|
|
236
|
+
async function onBackendKindChange(k: string) {
|
|
237
|
+
backendKind.value = k
|
|
238
|
+
await store.loadDescriptor(props.kind, k)
|
|
239
|
+
resetDraft()
|
|
240
|
+
}
|
|
241
|
+
|
|
224
242
|
async function testConfig(payload: {
|
|
225
243
|
config: Record<string, unknown>
|
|
226
244
|
secrets: Record<string, string>
|
|
@@ -332,9 +350,14 @@ function fieldHelp(key: string): string | undefined {
|
|
|
332
350
|
}}
|
|
333
351
|
</div>
|
|
334
352
|
|
|
335
|
-
<!-- Backend selector: the BYO manifest backend
|
|
336
|
-
|
|
337
|
-
|
|
353
|
+
<!-- Backend selector: the BYO manifest backend, a native Kubernetes backend, or a
|
|
354
|
+
programmatically-registered custom kind. -->
|
|
355
|
+
<UFormField :label="backendSelectorLabel">
|
|
356
|
+
<USelect
|
|
357
|
+
v-model="backendKind"
|
|
358
|
+
:items="backendKindItems"
|
|
359
|
+
@update:model-value="onBackendKindChange(String($event))"
|
|
360
|
+
/>
|
|
338
361
|
</UFormField>
|
|
339
362
|
|
|
340
363
|
<!-- Native Kubernetes runner backend (runner-pool). -->
|
|
@@ -51,8 +51,20 @@ const CONTRACTS = {
|
|
|
51
51
|
/** Environment-provider + runner-pool connection endpoints (self-describe + register/test). */
|
|
52
52
|
export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
53
53
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// `backendKind` (optional) describes a REGISTERED backend that isn't connected yet, so a
|
|
55
|
+
// custom kind's connect form renders before the first connect. Omitted ⇒ the stored kind.
|
|
56
|
+
// Branch on the kind so `send` sees a single concrete contract (a union contract can't
|
|
57
|
+
// type-check the optional `queryParams`).
|
|
58
|
+
describeProvider: (workspaceId: string, kind: ProviderConnectionKind, backendKind?: string) =>
|
|
59
|
+
kind === 'environment'
|
|
60
|
+
? send(CONTRACTS.environment.describe, {
|
|
61
|
+
pathPrefix: ws(workspaceId),
|
|
62
|
+
queryParams: { kind: backendKind },
|
|
63
|
+
})
|
|
64
|
+
: send(CONTRACTS['runner-pool'].describe, {
|
|
65
|
+
pathPrefix: ws(workspaceId),
|
|
66
|
+
queryParams: { kind: backendKind },
|
|
67
|
+
}),
|
|
56
68
|
|
|
57
69
|
getProviderConnection: (workspaceId: string, kind: ProviderConnectionKind) =>
|
|
58
70
|
send(CONTRACTS[kind].get, { pathPrefix: ws(workspaceId) }),
|
|
@@ -118,10 +130,11 @@ export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
|
118
130
|
/**
|
|
119
131
|
* Resolve the discriminated backend config (runner-pool OR environment) from a connect-form
|
|
120
132
|
* payload: an explicit `config` (the Kubernetes form) wins; otherwise a bare `manifest` (the
|
|
121
|
-
* manifest editor) is wrapped into the
|
|
122
|
-
*
|
|
133
|
+
* flat form / manifest editor) is wrapped into the selected backend kind — `body.backendKind`
|
|
134
|
+
* (a built-in `manifest` or a registered CUSTOM slug), defaulting to `manifest`. A custom kind
|
|
135
|
+
* MUST carry its slug here, else its flat-form save would be mis-tagged as the manifest backend.
|
|
123
136
|
*/
|
|
124
137
|
function backendConfig(body: RegisterProviderInput | TestProviderInput): Record<string, unknown> {
|
|
125
138
|
if (body.config) return body.config
|
|
126
|
-
return { kind: 'manifest', manifest: body.manifest ?? {} }
|
|
139
|
+
return { kind: body.backendKind ?? 'manifest', manifest: body.manifest ?? {} }
|
|
127
140
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, reactive, ref } from 'vue'
|
|
3
|
+
import type { BackendKindOption } from '@cat-factory/contracts'
|
|
3
4
|
import type {
|
|
4
5
|
ProviderConnection,
|
|
5
6
|
ProviderConnectionKind,
|
|
@@ -11,6 +12,21 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
11
12
|
|
|
12
13
|
const KINDS: ProviderConnectionKind[] = ['environment', 'runner-pool']
|
|
13
14
|
|
|
15
|
+
// Built-in fallback so the connect form's backend selector works before the snapshot
|
|
16
|
+
// loads (or on an older backend that doesn't advertise the kinds). The live lists come
|
|
17
|
+
// from the workspace snapshot (`environmentBackendKinds` / `runnerBackendKinds`) and may
|
|
18
|
+
// additionally carry a deployment's programmatically-registered CUSTOM kinds.
|
|
19
|
+
const BUILTIN_BACKEND_KINDS: Record<ProviderConnectionKind, BackendKindOption[]> = {
|
|
20
|
+
environment: [
|
|
21
|
+
{ kind: 'manifest', label: 'HTTP manifest' },
|
|
22
|
+
{ kind: 'kubernetes', label: 'Kubernetes' },
|
|
23
|
+
],
|
|
24
|
+
'runner-pool': [
|
|
25
|
+
{ kind: 'manifest', label: 'HTTP manifest pool' },
|
|
26
|
+
{ kind: 'kubernetes', label: 'Kubernetes' },
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
interface ProviderState {
|
|
15
31
|
/** null until first probed; false ⇒ integration disabled on the backend (hide it). */
|
|
16
32
|
available: boolean | null
|
|
@@ -38,13 +54,35 @@ export const useProviderConnectionsStore = defineStore('providerConnections', ()
|
|
|
38
54
|
})
|
|
39
55
|
const loaded = ref(false)
|
|
40
56
|
let inFlight: Promise<void> | null = null
|
|
57
|
+
// The selectable backend kinds per subsystem, fed from the workspace snapshot.
|
|
58
|
+
const backendKinds = reactive<Record<ProviderConnectionKind, BackendKindOption[]>>({
|
|
59
|
+
environment: BUILTIN_BACKEND_KINDS.environment,
|
|
60
|
+
'runner-pool': BUILTIN_BACKEND_KINDS['runner-pool'],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** Seed the backend-kind selector lists from the workspace snapshot (built-in + custom). */
|
|
64
|
+
function registerBackendKinds(
|
|
65
|
+
payload: Partial<Record<ProviderConnectionKind, BackendKindOption[]>>,
|
|
66
|
+
) {
|
|
67
|
+
for (const kind of KINDS) {
|
|
68
|
+
const list = payload[kind]
|
|
69
|
+
if (list && list.length > 0) backendKinds[kind] = list
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The selectable backend kinds for a subsystem (built-in fallback until the snapshot loads). */
|
|
74
|
+
function backendKindsFor(kind: ProviderConnectionKind): BackendKindOption[] {
|
|
75
|
+
return backendKinds[kind]
|
|
76
|
+
}
|
|
41
77
|
|
|
42
|
-
|
|
78
|
+
// `backendKind` (optional) re-probes the descriptor for a specific (e.g. not-yet-connected
|
|
79
|
+
// custom) backend kind, so its connect form renders before the first connect.
|
|
80
|
+
async function loadKind(kind: ProviderConnectionKind, backendKind?: string) {
|
|
43
81
|
const ws = useWorkspaceStore()
|
|
44
82
|
const s = state[kind]
|
|
45
83
|
try {
|
|
46
84
|
const [descriptor, { connection }] = await Promise.all([
|
|
47
|
-
api.describeProvider(ws.requireId(), kind),
|
|
85
|
+
api.describeProvider(ws.requireId(), kind, backendKind),
|
|
48
86
|
api.getProviderConnection(ws.requireId(), kind),
|
|
49
87
|
])
|
|
50
88
|
s.descriptor = descriptor
|
|
@@ -58,9 +96,24 @@ export const useProviderConnectionsStore = defineStore('providerConnections', ()
|
|
|
58
96
|
}
|
|
59
97
|
}
|
|
60
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Re-probe ONLY the descriptor for a specific backend kind (e.g. a not-yet-connected
|
|
101
|
+
* custom kind the user just picked), leaving the stored connection untouched. Switching
|
|
102
|
+
* the selector must NOT re-fetch the connection: that would reassign `state.connection`
|
|
103
|
+
* and bounce the selector back to the stored kind via the component's `connection` watch.
|
|
104
|
+
*/
|
|
105
|
+
async function loadDescriptor(kind: ProviderConnectionKind, backendKind?: string) {
|
|
106
|
+
const ws = useWorkspaceStore()
|
|
107
|
+
try {
|
|
108
|
+
state[kind].descriptor = await api.describeProvider(ws.requireId(), kind, backendKind)
|
|
109
|
+
} catch {
|
|
110
|
+
// Keep the existing descriptor/availability on a transient describe failure.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
61
114
|
/** Refresh both providers (used by the banner + after a save/remove). */
|
|
62
115
|
async function load() {
|
|
63
|
-
await Promise.all(KINDS.map(loadKind))
|
|
116
|
+
await Promise.all(KINDS.map((k) => loadKind(k)))
|
|
64
117
|
loaded.value = true
|
|
65
118
|
}
|
|
66
119
|
|
|
@@ -123,11 +176,14 @@ export const useProviderConnectionsStore = defineStore('providerConnections', ()
|
|
|
123
176
|
loaded,
|
|
124
177
|
load,
|
|
125
178
|
loadKind,
|
|
179
|
+
loadDescriptor,
|
|
126
180
|
ensureLoaded,
|
|
127
181
|
descriptorFor,
|
|
128
182
|
connectionFor,
|
|
129
183
|
isAvailable,
|
|
130
184
|
needingConfig,
|
|
185
|
+
backendKindsFor,
|
|
186
|
+
registerBackendKinds,
|
|
131
187
|
register,
|
|
132
188
|
updateSecrets,
|
|
133
189
|
test,
|
package/app/stores/workspace.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { useClarityStore } from '~/stores/clarity'
|
|
|
21
21
|
import { useBrainstormStore } from '~/stores/brainstorm'
|
|
22
22
|
import { useConsensusStore } from '~/stores/consensus'
|
|
23
23
|
import { useGitHubStore } from '~/stores/github'
|
|
24
|
+
import { useProviderConnectionsStore } from '~/stores/providerConnections'
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Owns the active workspace and bootstraps the app against the backend. On load
|
|
@@ -98,6 +99,13 @@ export const useWorkspaceStore = defineStore(
|
|
|
98
99
|
// Merge the deployment's registered custom agent kinds into the palette catalog so a
|
|
99
100
|
// proprietary kind renders as a first-class block + result view (idempotent on reload).
|
|
100
101
|
useAgentsStore().registerCustomKinds(snapshot.customAgentKinds ?? [])
|
|
102
|
+
// Seed the connect form's backend-kind selectors (built-in + any custom backend a
|
|
103
|
+
// deployment registered), so a programmatically-registered env/runner backend is a
|
|
104
|
+
// first-class connect option instead of a hardcoded manifest/kubernetes list.
|
|
105
|
+
useProviderConnectionsStore().registerBackendKinds({
|
|
106
|
+
environment: snapshot.environmentBackendKinds,
|
|
107
|
+
'runner-pool': snapshot.runnerBackendKinds,
|
|
108
|
+
})
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/** Resolve accounts + boards, then open the right board for the active account. */
|
|
@@ -50,8 +50,14 @@ export interface ProviderConnection {
|
|
|
50
50
|
*/
|
|
51
51
|
export interface RegisterProviderInput {
|
|
52
52
|
manifest?: Record<string, unknown>
|
|
53
|
-
/** The discriminated runner-backend config (manifest pool or
|
|
53
|
+
/** The discriminated runner-backend config (manifest pool, kubernetes, or a custom kind). */
|
|
54
54
|
config?: Record<string, unknown>
|
|
55
|
+
/**
|
|
56
|
+
* The selected backend kind, used to wrap a bare `manifest` into the discriminated config
|
|
57
|
+
* (`{ kind, manifest }`). Defaults to `manifest`. A CUSTOM registered kind passes its slug
|
|
58
|
+
* here so the flat-form save isn't mis-tagged as the built-in manifest backend.
|
|
59
|
+
*/
|
|
60
|
+
backendKind?: string
|
|
55
61
|
secrets: Record<string, string>
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -59,5 +65,6 @@ export interface RegisterProviderInput {
|
|
|
59
65
|
export interface TestProviderInput {
|
|
60
66
|
manifest?: Record<string, unknown>
|
|
61
67
|
config?: Record<string, unknown>
|
|
68
|
+
backendKind?: string
|
|
62
69
|
secrets?: Record<string, string>
|
|
63
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "^3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.
|
|
37
|
+
"@cat-factory/contracts": "0.55.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|