@cat-factory/app 0.51.1 → 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.
- package/app/components/layout/SideBar.vue +7 -3
- package/app/components/settings/InfrastructureWindow.vue +10 -5
- package/app/components/settings/ProviderConnectionTab.vue +46 -23
- package/app/components/testing/TestReportWindow.vue +79 -2
- package/app/composables/api/providerConnections.ts +18 -5
- package/app/stores/providerConnections.ts +59 -3
- package/app/stores/workspace.ts +8 -0
- package/app/types/providerConnections.ts +8 -1
- package/i18n/locales/en.json +7 -0
- package/i18n/locales/es.json +8 -1
- package/i18n/locales/fr.json +8 -1
- package/i18n/locales/pl.json +8 -1
- package/i18n/locales/uk.json +8 -1
- package/package.json +2 -2
|
@@ -24,11 +24,15 @@ const auth = useAuthStore()
|
|
|
24
24
|
const providerConnections = useProviderConnectionsStore()
|
|
25
25
|
const ui = useUiStore()
|
|
26
26
|
|
|
27
|
-
// The Infrastructure menu (agent-container execution + test environments)
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// The Infrastructure menu (agent-container execution + test environments) shows whenever the
|
|
28
|
+
// deployment reports its infrastructure capability — every facade populates `auth.infrastructure`
|
|
29
|
+
// (it drives the execution-backend selector), so there is always an execution + test-env backend
|
|
30
|
+
// to view, even on a Worker/Node deployment with no runner-pool/environment connection registered.
|
|
31
|
+
// The old provider-availability/local-mode signals stay as a defensive fallback for a backend that
|
|
32
|
+
// (somehow) omits the descriptor.
|
|
30
33
|
const showInfrastructure = computed(
|
|
31
34
|
() =>
|
|
35
|
+
auth.infrastructure != null ||
|
|
32
36
|
auth.localMode?.enabled === true ||
|
|
33
37
|
providerConnections.isAvailable('runner-pool') ||
|
|
34
38
|
providerConnections.isAvailable('environment'),
|
|
@@ -29,9 +29,12 @@ const open = computed({
|
|
|
29
29
|
|
|
30
30
|
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// The tabs are driven by the deployment's infrastructure capability (every facade reports
|
|
33
|
+
// execution + test-env backends), NOT the optional provider-connection probes — the execution-
|
|
34
|
+
// backend selector must show even when no runner-pool / environment connection is registered.
|
|
35
|
+
// The connect form inside each tab still gates on its own probe (see the template).
|
|
36
|
+
const agentsAvailable = computed(() => (auth.infrastructure?.execution.available.length ?? 0) > 0)
|
|
37
|
+
const envsAvailable = computed(() => (auth.infrastructure?.testEnv.available.length ?? 0) > 0)
|
|
35
38
|
|
|
36
39
|
const tabs = computed(() => {
|
|
37
40
|
const out: { value: ProviderConnectionKind; label: string; icon: string; slot: string }[] = []
|
|
@@ -102,7 +105,8 @@ watch([tabs, () => store.loaded], () => {
|
|
|
102
105
|
<div class="space-y-4">
|
|
103
106
|
<!-- Where agent containers run (writable in local mode; read-only elsewhere). -->
|
|
104
107
|
<ExecutionBackendSelector axis="execution" />
|
|
105
|
-
|
|
108
|
+
<!-- The runner-pool connect form only when that integration is enabled. -->
|
|
109
|
+
<ProviderConnectionTab v-if="store.isAvailable('runner-pool')" kind="runner-pool" />
|
|
106
110
|
<!-- Local mode: the warm-pool + checkout reuse ARE the host agent-container
|
|
107
111
|
runtime, so they live here rather than in a separate menu. -->
|
|
108
112
|
<section v-if="isLocal" class="border-t border-slate-800 pt-4">
|
|
@@ -117,7 +121,8 @@ watch([tabs, () => store.loaded], () => {
|
|
|
117
121
|
<div class="space-y-4">
|
|
118
122
|
<!-- Where Tester environments run (writable in local mode; read-only elsewhere). -->
|
|
119
123
|
<ExecutionBackendSelector axis="testEnv" />
|
|
120
|
-
|
|
124
|
+
<!-- The environment-provider connect form only when that integration is enabled. -->
|
|
125
|
+
<ProviderConnectionTab v-if="store.isAvailable('environment')" kind="environment" />
|
|
121
126
|
</div>
|
|
122
127
|
</template>
|
|
123
128
|
</UTabs>
|
|
@@ -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). -->
|
|
@@ -23,7 +23,7 @@ import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDr
|
|
|
23
23
|
|
|
24
24
|
const board = useBoardStore()
|
|
25
25
|
const execution = useExecutionStore()
|
|
26
|
-
const { t, d } = useI18n()
|
|
26
|
+
const { t, d, n } = useI18n()
|
|
27
27
|
|
|
28
28
|
// Per-window blob cache for the captured screenshots; revoked on unmount.
|
|
29
29
|
const blobs = useArtifactBlobs()
|
|
@@ -58,6 +58,14 @@ const executionId = computed(() => instance.value?.id ?? null)
|
|
|
58
58
|
// The infra-attempts log drawer is opened on demand (it fetches the per-run log rows).
|
|
59
59
|
const showProvisioning = ref(false)
|
|
60
60
|
|
|
61
|
+
// The in-container docker-compose dependency stand-up record (local-infra tester): whether
|
|
62
|
+
// the dependencies came up + the captured `docker compose up` logs. Unlike the provisioning
|
|
63
|
+
// drawer above (the orchestrator-side container/env spin-up), this is the stand-up that runs
|
|
64
|
+
// INSIDE the container — the highest-signal artifact when local infra fails to come up.
|
|
65
|
+
const infraSetup = computed(() => testState.value?.infraSetup ?? null)
|
|
66
|
+
// The captured stand-up logs are shown on demand (they can be long).
|
|
67
|
+
const showInfraSetupLogs = ref(false)
|
|
68
|
+
|
|
61
69
|
const screenshots = computed<TestScreenshot[]>(() => report.value?.screenshots ?? [])
|
|
62
70
|
// Resolve each capture into an object URL for the gallery + lightbox. The shared cache
|
|
63
71
|
// dedupes, so the lightbox reuses what the thumbnails fetched. (The reference design is not
|
|
@@ -338,7 +346,7 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
338
346
|
environment. A no-infra tester (no container, no env) has no infra attempts
|
|
339
347
|
either, so we don't render an empty header + a log toggle over nothing. -->
|
|
340
348
|
<section
|
|
341
|
-
v-if="step && (step.container || stepEnvironment)"
|
|
349
|
+
v-if="step && (step.container || stepEnvironment || infraSetup)"
|
|
342
350
|
data-testid="tester-infrastructure"
|
|
343
351
|
class="space-y-3"
|
|
344
352
|
>
|
|
@@ -347,6 +355,75 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
347
355
|
</h3>
|
|
348
356
|
<StepContainerStatus :step="step" :run-failed="runFailed" />
|
|
349
357
|
<EnvironmentStatusPanel v-if="stepEnvironment" :environment="stepEnvironment" />
|
|
358
|
+
|
|
359
|
+
<!-- In-container docker-compose dependency stand-up (local-infra tester): the
|
|
360
|
+
outcome + the captured `docker compose up` logs. This is the stand-up that
|
|
361
|
+
runs INSIDE the container, so its output isn't in the provisioning drawer
|
|
362
|
+
below — it's the highest-signal artifact when local infra fails to start. -->
|
|
363
|
+
<div
|
|
364
|
+
v-if="infraSetup"
|
|
365
|
+
data-testid="tester-infra-setup"
|
|
366
|
+
class="rounded-lg border px-3 py-2"
|
|
367
|
+
:class="
|
|
368
|
+
infraSetup.started
|
|
369
|
+
? 'border-slate-800 bg-slate-900/60'
|
|
370
|
+
: 'border-rose-500/40 bg-rose-500/10'
|
|
371
|
+
"
|
|
372
|
+
>
|
|
373
|
+
<div class="flex items-center gap-2">
|
|
374
|
+
<UIcon
|
|
375
|
+
:name="infraSetup.started ? 'i-lucide-container' : 'i-lucide-circle-x'"
|
|
376
|
+
class="h-3.5 w-3.5 shrink-0"
|
|
377
|
+
:class="infraSetup.started ? 'text-emerald-400' : 'text-rose-400'"
|
|
378
|
+
/>
|
|
379
|
+
<span class="text-[13px] font-medium text-slate-200">
|
|
380
|
+
{{ infraSetup.started ? t('testing.standup.up') : t('testing.standup.failed') }}
|
|
381
|
+
</span>
|
|
382
|
+
<span
|
|
383
|
+
v-if="infraSetup.durationMs != null"
|
|
384
|
+
class="ml-auto text-[11px] text-slate-500"
|
|
385
|
+
>
|
|
386
|
+
{{
|
|
387
|
+
t('testing.standup.took', {
|
|
388
|
+
seconds: n(infraSetup.durationMs / 1000, 'decimal'),
|
|
389
|
+
})
|
|
390
|
+
}}
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<p v-if="infraSetup.composePath" class="mt-1 font-mono text-[11px] text-slate-500">
|
|
394
|
+
{{ infraSetup.composePath }}
|
|
395
|
+
</p>
|
|
396
|
+
<p
|
|
397
|
+
v-if="infraSetup.error"
|
|
398
|
+
class="mt-1 text-[12px] leading-snug text-rose-300"
|
|
399
|
+
data-testid="tester-infra-setup-error"
|
|
400
|
+
>
|
|
401
|
+
{{ infraSetup.error }}
|
|
402
|
+
</p>
|
|
403
|
+
<template v-if="infraSetup.logs">
|
|
404
|
+
<UButton
|
|
405
|
+
:icon="showInfraSetupLogs ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
|
|
406
|
+
variant="ghost"
|
|
407
|
+
size="xs"
|
|
408
|
+
class="mt-1.5"
|
|
409
|
+
data-testid="tester-infra-setup-logs-toggle"
|
|
410
|
+
@click="showInfraSetupLogs = !showInfraSetupLogs"
|
|
411
|
+
>
|
|
412
|
+
{{
|
|
413
|
+
showInfraSetupLogs
|
|
414
|
+
? t('testing.standup.hideLogs')
|
|
415
|
+
: t('testing.standup.showLogs')
|
|
416
|
+
}}
|
|
417
|
+
</UButton>
|
|
418
|
+
<pre
|
|
419
|
+
v-if="showInfraSetupLogs"
|
|
420
|
+
data-testid="tester-infra-setup-logs"
|
|
421
|
+
class="mt-2 max-h-64 overflow-auto rounded bg-slate-950/70 p-2 font-mono text-[11px] leading-relaxed text-slate-300"
|
|
422
|
+
>{{ infraSetup.logs }}</pre
|
|
423
|
+
>
|
|
424
|
+
</template>
|
|
425
|
+
</div>
|
|
426
|
+
|
|
350
427
|
<div v-if="executionId">
|
|
351
428
|
<UButton
|
|
352
429
|
:icon="showProvisioning ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
|
|
@@ -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/i18n/locales/en.json
CHANGED
|
@@ -2999,6 +2999,13 @@
|
|
|
2999
2999
|
},
|
|
3000
3000
|
"environment": "Environment",
|
|
3001
3001
|
"infrastructure": "Infrastructure",
|
|
3002
|
+
"standup": {
|
|
3003
|
+
"up": "Dependencies started",
|
|
3004
|
+
"failed": "Dependencies failed to start",
|
|
3005
|
+
"took": "Took {seconds}s",
|
|
3006
|
+
"showLogs": "Show stand-up logs",
|
|
3007
|
+
"hideLogs": "Hide stand-up logs"
|
|
3008
|
+
},
|
|
3002
3009
|
"footer": "Scenarios are the areas the Tester chose to exercise (its spec acceptance scenarios). Outcomes and concerns are grouped under them by name.",
|
|
3003
3010
|
"@screenshotAlt": {
|
|
3004
3011
|
"description": "Alt text for a captured screenshot thumbnail; {view} is the screen/view name. The literal word 'screenshot' should be localized."
|
package/i18n/locales/es.json
CHANGED
|
@@ -2912,7 +2912,14 @@
|
|
|
2912
2912
|
},
|
|
2913
2913
|
"environment": "Entorno",
|
|
2914
2914
|
"footer": "Los escenarios son las áreas que el Tester decidió ejercitar (sus escenarios de aceptación de la especificación). Los resultados y las incidencias se agrupan bajo ellos por nombre.",
|
|
2915
|
-
"infrastructure": "Infraestructura"
|
|
2915
|
+
"infrastructure": "Infraestructura",
|
|
2916
|
+
"standup": {
|
|
2917
|
+
"up": "Dependencias iniciadas",
|
|
2918
|
+
"failed": "No se pudieron iniciar las dependencias",
|
|
2919
|
+
"took": "Tardó {seconds}s",
|
|
2920
|
+
"showLogs": "Mostrar registros de arranque",
|
|
2921
|
+
"hideLogs": "Ocultar registros de arranque"
|
|
2922
|
+
}
|
|
2916
2923
|
},
|
|
2917
2924
|
"visualConfirm": {
|
|
2918
2925
|
"ariaLabel": "Confirmación visual",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -2912,7 +2912,14 @@
|
|
|
2912
2912
|
},
|
|
2913
2913
|
"environment": "Environnement",
|
|
2914
2914
|
"footer": "Les scénarios sont les domaines que le Testeur a choisi d'éprouver (ses scénarios d'acceptation de la spécification). Les résultats et les réserves y sont regroupés par nom.",
|
|
2915
|
-
"infrastructure": "Infrastructure"
|
|
2915
|
+
"infrastructure": "Infrastructure",
|
|
2916
|
+
"standup": {
|
|
2917
|
+
"up": "Dépendances démarrées",
|
|
2918
|
+
"failed": "Échec du démarrage des dépendances",
|
|
2919
|
+
"took": "Durée : {seconds}s",
|
|
2920
|
+
"showLogs": "Afficher les journaux de démarrage",
|
|
2921
|
+
"hideLogs": "Masquer les journaux de démarrage"
|
|
2922
|
+
}
|
|
2916
2923
|
},
|
|
2917
2924
|
"visualConfirm": {
|
|
2918
2925
|
"ariaLabel": "Confirmation visuelle",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -2912,7 +2912,14 @@
|
|
|
2912
2912
|
},
|
|
2913
2913
|
"environment": "Środowisko",
|
|
2914
2914
|
"footer": "Scenariusze to obszary, które Tester postanowił sprawdzić (jego scenariusze akceptacyjne ze specyfikacji). Wyniki i zastrzeżenia są pod nimi grupowane według nazwy.",
|
|
2915
|
-
"infrastructure": "Infrastruktura"
|
|
2915
|
+
"infrastructure": "Infrastruktura",
|
|
2916
|
+
"standup": {
|
|
2917
|
+
"up": "Zależności uruchomione",
|
|
2918
|
+
"failed": "Nie udało się uruchomić zależności",
|
|
2919
|
+
"took": "Czas: {seconds}s",
|
|
2920
|
+
"showLogs": "Pokaż dzienniki uruchamiania",
|
|
2921
|
+
"hideLogs": "Ukryj dzienniki uruchamiania"
|
|
2922
|
+
}
|
|
2916
2923
|
},
|
|
2917
2924
|
"visualConfirm": {
|
|
2918
2925
|
"ariaLabel": "Potwierdzenie wizualne",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -2912,7 +2912,14 @@
|
|
|
2912
2912
|
},
|
|
2913
2913
|
"environment": "Середовище",
|
|
2914
2914
|
"footer": "Сценарії — це області, які Тестувальник вирішив перевірити (його сценарії приймання зі специфікації). Результати та зауваження групуються під ними за назвою.",
|
|
2915
|
-
"infrastructure": "Інфраструктура"
|
|
2915
|
+
"infrastructure": "Інфраструктура",
|
|
2916
|
+
"standup": {
|
|
2917
|
+
"up": "Залежності запущено",
|
|
2918
|
+
"failed": "Не вдалося запустити залежності",
|
|
2919
|
+
"took": "Час: {seconds}s",
|
|
2920
|
+
"showLogs": "Показати журнали запуску",
|
|
2921
|
+
"hideLogs": "Сховати журнали запуску"
|
|
2922
|
+
}
|
|
2916
2923
|
},
|
|
2917
2924
|
"visualConfirm": {
|
|
2918
2925
|
"ariaLabel": "Візуальне підтвердження",
|
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",
|