@cat-factory/app 0.77.0 → 0.79.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/gates/GateFailingCheckList.vue +62 -0
- package/app/components/gates/GateResultView.vue +43 -52
- package/app/components/layout/IntegrationsHub.vue +23 -0
- package/app/components/panels/AgentStepDetail.vue +41 -0
- package/app/components/panels/AttemptEntryHeader.vue +36 -0
- package/app/components/panels/inspector/FrontendBindingsResolved.vue +111 -0
- package/app/components/panels/inspector/FrontendConfig.vue +8 -0
- package/app/components/settings/PackageRegistriesPanel.vue +222 -0
- package/app/components/testing/TestReportWindow.vue +13 -24
- package/app/composables/api/environments.ts +10 -0
- package/app/composables/api/packageRegistries.ts +24 -0
- package/app/composables/useApi.ts +4 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/environments.ts +52 -0
- package/app/stores/packageRegistries.ts +66 -0
- package/app/stores/ui.ts +13 -0
- package/app/types/domain.ts +2 -0
- package/app/types/packageRegistries.ts +13 -0
- package/i18n/locales/en.json +38 -0
- package/i18n/locales/es.json +38 -0
- package/i18n/locales/fr.json +38 -0
- package/i18n/locales/he.json +38 -0
- package/i18n/locales/ja.json +38 -0
- package/i18n/locales/pl.json +38 -0
- package/i18n/locales/tr.json +38 -0
- package/i18n/locales/uk.json +38 -0
- package/package.json +2 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Private package registries — the workspace's npm-registry entries (npm private
|
|
3
|
+
// orgs, GitHub Packages) that agent containers use to resolve private dependencies
|
|
4
|
+
// on checkout. Tokens are write-only: the list renders from the redacted summary
|
|
5
|
+
// (vendor + scopes + token tail) and an entry is edited by deleting + re-adding.
|
|
6
|
+
// Opened from the Integrations hub.
|
|
7
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
8
|
+
import type { PackageRegistryVendor } from '~/types/packageRegistries'
|
|
9
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
|
+
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
const ui = useUiStore()
|
|
13
|
+
const store = usePackageRegistriesStore()
|
|
14
|
+
const toast = useToast()
|
|
15
|
+
const { confirmAction, toastDone } = useConfirmAction()
|
|
16
|
+
|
|
17
|
+
const open = computed({
|
|
18
|
+
get: () => ui.packageRegistriesOpen,
|
|
19
|
+
set: (v: boolean) => (v ? ui.openPackageRegistries() : ui.closePackageRegistries()),
|
|
20
|
+
})
|
|
21
|
+
const back = useIntegrationBack(open)
|
|
22
|
+
|
|
23
|
+
// The registry vendors a workspace can connect. Fixed set — the host derives from the
|
|
24
|
+
// vendor server-side, so it renders read-only here. Vendor names stay verbatim.
|
|
25
|
+
const VENDORS: { value: PackageRegistryVendor; label: string; host: string }[] = [
|
|
26
|
+
{ value: 'npmjs', label: 'npm (npmjs.com)', host: 'registry.npmjs.org' },
|
|
27
|
+
{ value: 'github-packages', label: 'GitHub Packages', host: 'npm.pkg.github.com' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const form = reactive({
|
|
31
|
+
vendor: 'npmjs' as PackageRegistryVendor,
|
|
32
|
+
scopes: '',
|
|
33
|
+
token: '',
|
|
34
|
+
})
|
|
35
|
+
const busy = ref(false)
|
|
36
|
+
|
|
37
|
+
const vendorHost = computed(() => VENDORS.find((v) => v.value === form.vendor)?.host ?? '')
|
|
38
|
+
|
|
39
|
+
function vendorLabel(vendor: PackageRegistryVendor): string {
|
|
40
|
+
return VENDORS.find((v) => v.value === vendor)?.label ?? vendor
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Parse the comma/space-separated scopes input into `@org` entries. */
|
|
44
|
+
const parsedScopes = computed(() =>
|
|
45
|
+
form.scopes
|
|
46
|
+
.split(/[\s,]+/)
|
|
47
|
+
.map((s) => s.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((s) => (s.startsWith('@') ? s : `@${s}`)),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
function notifyError(title: string, e: unknown) {
|
|
53
|
+
toast.add({
|
|
54
|
+
title,
|
|
55
|
+
description: e instanceof Error ? e.message : String(e),
|
|
56
|
+
icon: 'i-lucide-triangle-alert',
|
|
57
|
+
color: 'error',
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
watch(
|
|
62
|
+
open,
|
|
63
|
+
async (isOpen) => {
|
|
64
|
+
if (!isOpen) return
|
|
65
|
+
try {
|
|
66
|
+
await store.ensureLoaded()
|
|
67
|
+
} catch (e) {
|
|
68
|
+
notifyError(t('settings.packageRegistries.toast.loadFailed'), e)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{ immediate: true },
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async function addEntry() {
|
|
75
|
+
busy.value = true
|
|
76
|
+
try {
|
|
77
|
+
await store.add({
|
|
78
|
+
ecosystem: 'npm',
|
|
79
|
+
vendor: form.vendor,
|
|
80
|
+
scopes: parsedScopes.value,
|
|
81
|
+
token: form.token.trim(),
|
|
82
|
+
})
|
|
83
|
+
form.scopes = ''
|
|
84
|
+
form.token = ''
|
|
85
|
+
toast.add({
|
|
86
|
+
title: t('settings.packageRegistries.toast.added'),
|
|
87
|
+
icon: 'i-lucide-check',
|
|
88
|
+
color: 'success',
|
|
89
|
+
})
|
|
90
|
+
} catch (e) {
|
|
91
|
+
notifyError(t('settings.packageRegistries.toast.addFailed'), e)
|
|
92
|
+
} finally {
|
|
93
|
+
busy.value = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function removeEntry(entryId: string) {
|
|
98
|
+
const noun = t('settings.packageRegistries.entryNoun')
|
|
99
|
+
if (!(await confirmAction('remove', noun))) return
|
|
100
|
+
busy.value = true
|
|
101
|
+
try {
|
|
102
|
+
await store.remove(entryId)
|
|
103
|
+
toastDone('remove', noun)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
notifyError(t('settings.packageRegistries.toast.removeFailed'), e)
|
|
106
|
+
} finally {
|
|
107
|
+
busy.value = false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<template>
|
|
113
|
+
<UModal
|
|
114
|
+
v-model:open="open"
|
|
115
|
+
:title="t('settings.packageRegistries.title')"
|
|
116
|
+
:ui="{ content: 'max-w-lg' }"
|
|
117
|
+
>
|
|
118
|
+
<template #title>
|
|
119
|
+
<IntegrationBackTitle :title="t('settings.packageRegistries.title')" @back="back" />
|
|
120
|
+
</template>
|
|
121
|
+
<template #body>
|
|
122
|
+
<div class="space-y-4" data-testid="package-registries-panel">
|
|
123
|
+
<p class="text-sm text-slate-400">
|
|
124
|
+
{{ t('settings.packageRegistries.intro') }}
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
<section
|
|
128
|
+
v-if="store.entries.length"
|
|
129
|
+
class="space-y-2 rounded-lg border border-slate-700 p-3"
|
|
130
|
+
>
|
|
131
|
+
<h3 class="text-sm font-semibold">
|
|
132
|
+
{{ t('settings.packageRegistries.list.heading') }}
|
|
133
|
+
</h3>
|
|
134
|
+
<div
|
|
135
|
+
v-for="entry in store.entries"
|
|
136
|
+
:key="entry.id"
|
|
137
|
+
class="flex items-center justify-between gap-2 rounded-md border border-slate-800 px-3 py-2"
|
|
138
|
+
>
|
|
139
|
+
<div class="min-w-0 space-y-1">
|
|
140
|
+
<div class="flex items-center gap-2">
|
|
141
|
+
<span class="text-sm font-medium">{{ vendorLabel(entry.vendor) }}</span>
|
|
142
|
+
<span class="text-[11px] text-slate-500">
|
|
143
|
+
{{ t('settings.packageRegistries.list.tokenTail', { tail: entry.tokenTail }) }}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex flex-wrap gap-1">
|
|
147
|
+
<UBadge
|
|
148
|
+
v-for="scope in entry.scopes"
|
|
149
|
+
:key="scope"
|
|
150
|
+
color="neutral"
|
|
151
|
+
variant="soft"
|
|
152
|
+
size="sm"
|
|
153
|
+
>
|
|
154
|
+
{{ scope }}
|
|
155
|
+
</UBadge>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<UButton
|
|
159
|
+
color="error"
|
|
160
|
+
variant="ghost"
|
|
161
|
+
icon="i-lucide-trash-2"
|
|
162
|
+
size="sm"
|
|
163
|
+
:loading="busy"
|
|
164
|
+
:data-testid="`package-registry-delete-${entry.id}`"
|
|
165
|
+
:aria-label="t('settings.packageRegistries.list.remove')"
|
|
166
|
+
@click="removeEntry(entry.id)"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</section>
|
|
170
|
+
|
|
171
|
+
<section class="space-y-3 rounded-lg border border-slate-700 p-3">
|
|
172
|
+
<h3 class="text-sm font-semibold">
|
|
173
|
+
{{ t('settings.packageRegistries.add.heading') }}
|
|
174
|
+
</h3>
|
|
175
|
+
|
|
176
|
+
<UFormField :label="t('settings.packageRegistries.add.vendor')">
|
|
177
|
+
<USelect
|
|
178
|
+
v-model="form.vendor"
|
|
179
|
+
:items="VENDORS"
|
|
180
|
+
value-key="value"
|
|
181
|
+
class="w-full"
|
|
182
|
+
data-testid="package-registry-vendor"
|
|
183
|
+
/>
|
|
184
|
+
</UFormField>
|
|
185
|
+
<p class="text-[11px] text-slate-500">
|
|
186
|
+
{{ t('settings.packageRegistries.add.host', { host: vendorHost }) }}
|
|
187
|
+
</p>
|
|
188
|
+
|
|
189
|
+
<UFormField
|
|
190
|
+
:label="t('settings.packageRegistries.add.scopes')"
|
|
191
|
+
:help="t('settings.packageRegistries.add.scopesHelp')"
|
|
192
|
+
>
|
|
193
|
+
<UInput
|
|
194
|
+
v-model="form.scopes"
|
|
195
|
+
placeholder="@my-org, @my-other-org"
|
|
196
|
+
class="w-full"
|
|
197
|
+
data-testid="package-registry-scopes"
|
|
198
|
+
/>
|
|
199
|
+
</UFormField>
|
|
200
|
+
|
|
201
|
+
<UFormField :label="t('settings.packageRegistries.add.token')">
|
|
202
|
+
<UInput
|
|
203
|
+
v-model="form.token"
|
|
204
|
+
type="password"
|
|
205
|
+
class="w-full"
|
|
206
|
+
data-testid="package-registry-token"
|
|
207
|
+
/>
|
|
208
|
+
</UFormField>
|
|
209
|
+
|
|
210
|
+
<UButton
|
|
211
|
+
:loading="busy"
|
|
212
|
+
:disabled="!parsedScopes.length || !form.token.trim()"
|
|
213
|
+
data-testid="package-registry-save"
|
|
214
|
+
@click="addEntry"
|
|
215
|
+
>
|
|
216
|
+
{{ t('settings.packageRegistries.add.save') }}
|
|
217
|
+
</UButton>
|
|
218
|
+
</section>
|
|
219
|
+
</div>
|
|
220
|
+
</template>
|
|
221
|
+
</UModal>
|
|
222
|
+
</template>
|
|
@@ -18,6 +18,7 @@ import ArtifactLightbox from '~/components/media/ArtifactLightbox.vue'
|
|
|
18
18
|
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
19
19
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
20
20
|
import StepContainerStatus from '~/components/panels/StepContainerStatus.vue'
|
|
21
|
+
import AttemptEntryHeader from '~/components/panels/AttemptEntryHeader.vue'
|
|
21
22
|
import EnvironmentStatusPanel from '~/components/environments/EnvironmentStatusPanel.vue'
|
|
22
23
|
import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
|
|
23
24
|
|
|
@@ -507,30 +508,18 @@ const GROUP_STATUS_META: Record<ScenarioGroup['status'], { icon: string; text: s
|
|
|
507
508
|
data-testid="tester-fixer-attempt"
|
|
508
509
|
class="rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2"
|
|
509
510
|
>
|
|
510
|
-
<
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
size="sm"
|
|
523
|
-
>
|
|
524
|
-
{{
|
|
525
|
-
a.outcome === 'completed'
|
|
526
|
-
? t('testing.fixerTimeline.completed')
|
|
527
|
-
: t('testing.fixerTimeline.failed')
|
|
528
|
-
}}
|
|
529
|
-
</UBadge>
|
|
530
|
-
<span class="ms-auto text-[11px] text-slate-500">{{
|
|
531
|
-
d(new Date(a.at), 'short')
|
|
532
|
-
}}</span>
|
|
533
|
-
</div>
|
|
511
|
+
<AttemptEntryHeader
|
|
512
|
+
:label="t('testing.fixerTimeline.attempt', { n: a.attempt })"
|
|
513
|
+
:outcome="a.outcome"
|
|
514
|
+
:outcome-label="
|
|
515
|
+
a.outcome === 'completed'
|
|
516
|
+
? t('testing.fixerTimeline.completed')
|
|
517
|
+
: t('testing.fixerTimeline.failed')
|
|
518
|
+
"
|
|
519
|
+
:at="a.at"
|
|
520
|
+
:icon="a.outcome === 'completed' ? 'i-lucide-wrench' : 'i-lucide-circle-x'"
|
|
521
|
+
:icon-class="a.outcome === 'completed' ? 'text-amber-300' : 'text-rose-400'"
|
|
522
|
+
/>
|
|
534
523
|
<p v-if="a.summary" class="mt-1 text-[12px] leading-snug text-slate-400">
|
|
535
524
|
{{ a.summary }}
|
|
536
525
|
</p>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { listEnvironmentsContract } from '@cat-factory/contracts'
|
|
2
|
+
import type { ApiContext } from './context'
|
|
3
|
+
|
|
4
|
+
/** Ephemeral environments: the workspace's live env handles (used to resolve frontend bindings). */
|
|
5
|
+
export function environmentsApi({ send, ws }: ApiContext) {
|
|
6
|
+
return {
|
|
7
|
+
listEnvironments: (workspaceId: string) =>
|
|
8
|
+
send(listEnvironmentsContract, { pathPrefix: ws(workspaceId) }),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addPackageRegistryContract,
|
|
3
|
+
deletePackageRegistryContract,
|
|
4
|
+
listPackageRegistriesContract,
|
|
5
|
+
} from '@cat-factory/contracts'
|
|
6
|
+
import type { AddPackageRegistryInput } from '~/types/packageRegistries'
|
|
7
|
+
import type { ApiContext } from './context'
|
|
8
|
+
|
|
9
|
+
/** Private package registries: the workspace's entries agent containers install with. */
|
|
10
|
+
export function packageRegistriesApi({ send, ws }: ApiContext) {
|
|
11
|
+
return {
|
|
12
|
+
listPackageRegistries: (workspaceId: string) =>
|
|
13
|
+
send(listPackageRegistriesContract, { pathPrefix: ws(workspaceId) }),
|
|
14
|
+
|
|
15
|
+
addPackageRegistry: (workspaceId: string, body: AddPackageRegistryInput) =>
|
|
16
|
+
send(addPackageRegistryContract, { pathPrefix: ws(workspaceId), body }),
|
|
17
|
+
|
|
18
|
+
deletePackageRegistry: (workspaceId: string, entryId: string) =>
|
|
19
|
+
send(deletePackageRegistryContract, {
|
|
20
|
+
pathPrefix: ws(workspaceId),
|
|
21
|
+
pathParams: { entryId },
|
|
22
|
+
}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -18,11 +18,13 @@ import { kaizenApi } from './api/kaizen'
|
|
|
18
18
|
import { localSettingsApi } from './api/localSettings'
|
|
19
19
|
import { modelsApi } from './api/models'
|
|
20
20
|
import { notificationsApi } from './api/notifications'
|
|
21
|
+
import { packageRegistriesApi } from './api/packageRegistries'
|
|
21
22
|
import { presetsApi } from './api/presets'
|
|
22
23
|
import { providerConnectionsApi } from './api/providerConnections'
|
|
23
24
|
import { provisioningLogsApi } from './api/provisioningLogs'
|
|
24
25
|
import { recurringApi } from './api/recurring'
|
|
25
26
|
import { previewApi } from './api/preview'
|
|
27
|
+
import { environmentsApi } from './api/environments'
|
|
26
28
|
import { releaseHealthApi } from './api/releaseHealth'
|
|
27
29
|
import { sandboxApi } from './api/sandbox'
|
|
28
30
|
import { reviewsApi } from './api/reviews'
|
|
@@ -112,7 +114,9 @@ export function useApi() {
|
|
|
112
114
|
...infraHandlersApi(ctx),
|
|
113
115
|
...provisioningLogsApi(ctx),
|
|
114
116
|
...releaseHealthApi(ctx),
|
|
117
|
+
...packageRegistriesApi(ctx),
|
|
115
118
|
...previewApi(ctx),
|
|
119
|
+
...environmentsApi(ctx),
|
|
116
120
|
...recurringApi(ctx),
|
|
117
121
|
...sandboxApi(ctx),
|
|
118
122
|
...githubApi(ctx),
|
package/app/pages/index.vue
CHANGED
|
@@ -85,6 +85,9 @@ const AccountSettingsPanel = defineAsyncComponent(
|
|
|
85
85
|
const ObservabilityConnectionPanel = defineAsyncComponent(
|
|
86
86
|
() => import('~/components/settings/ObservabilityConnectionPanel.vue'),
|
|
87
87
|
)
|
|
88
|
+
const PackageRegistriesPanel = defineAsyncComponent(
|
|
89
|
+
() => import('~/components/settings/PackageRegistriesPanel.vue'),
|
|
90
|
+
)
|
|
88
91
|
const InfrastructureWindow = defineAsyncComponent(
|
|
89
92
|
() => import('~/components/settings/InfrastructureWindow.vue'),
|
|
90
93
|
)
|
|
@@ -356,6 +359,7 @@ watch(
|
|
|
356
359
|
<WorkspaceSettingsPanel v-if="ui.workspaceSettingsOpen" />
|
|
357
360
|
<AccountSettingsPanel v-if="ui.accountSettingsOpen" />
|
|
358
361
|
<ObservabilityConnectionPanel v-if="ui.observabilityConnectionOpen" />
|
|
362
|
+
<PackageRegistriesPanel v-if="ui.packageRegistriesOpen" />
|
|
359
363
|
<InfrastructureWindow v-if="ui.infrastructureOpen" />
|
|
360
364
|
<ModelConfigurationPanel v-if="ui.modelConfigOpen" />
|
|
361
365
|
<LocalModelEndpointsPanel v-if="ui.localModelsOpen" />
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
boundServiceFrameIds,
|
|
5
|
+
indexLiveServiceEnvUrls,
|
|
6
|
+
type FrontendConfig,
|
|
7
|
+
} from '@cat-factory/contracts'
|
|
8
|
+
import type { EnvironmentHandle } from '~/types/domain'
|
|
9
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The workspace's live ephemeral-environment handles, fetched on demand from
|
|
13
|
+
* `GET /workspaces/:ws/environments`. Used to resolve a `frontend` frame's backend bindings to
|
|
14
|
+
* their live service URLs (the SPA mirror of the backend's `AgentContextBuilder` resolution) — so
|
|
15
|
+
* the inspector and the run/step detail can show each `envVar → live URL | mocked` the SAME way a
|
|
16
|
+
* UI-test run would. Kept as a thin, load-on-open cache (no snapshot delivery, no self-poll):
|
|
17
|
+
* the callers refresh it when the frontend inspector / a UI-test step detail opens.
|
|
18
|
+
*/
|
|
19
|
+
export const useEnvironmentsStore = defineStore('environments', () => {
|
|
20
|
+
const api = useApi()
|
|
21
|
+
|
|
22
|
+
/** The last-fetched env handles for the current workspace. */
|
|
23
|
+
const handles = ref<EnvironmentHandle[]>([])
|
|
24
|
+
/** A load is in flight (drives an optional spinner). */
|
|
25
|
+
const loading = ref(false)
|
|
26
|
+
|
|
27
|
+
/** (Re)load the workspace's environment handles; failures leave the last-known list. */
|
|
28
|
+
async function load(): Promise<void> {
|
|
29
|
+
const ws = useWorkspaceStore()
|
|
30
|
+
loading.value = true
|
|
31
|
+
try {
|
|
32
|
+
handles.value = await api.listEnvironments(ws.requireId())
|
|
33
|
+
} catch {
|
|
34
|
+
// Transient: keep the last-known handles rather than blanking the resolved view.
|
|
35
|
+
} finally {
|
|
36
|
+
loading.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The live `serviceFrameId → url` map for exactly the service FRAMES a frontend config binds —
|
|
42
|
+
* the same newest-wins, ready-with-URL indexing the backend applies (`indexLiveServiceEnvUrls`),
|
|
43
|
+
* so the SPA's resolved-binding view can't drift from what a run would resolve.
|
|
44
|
+
*/
|
|
45
|
+
function liveServiceEnvUrls(
|
|
46
|
+
config: Pick<FrontendConfig, 'backendBindings'>,
|
|
47
|
+
): Map<string, string> {
|
|
48
|
+
return indexLiveServiceEnvUrls(handles.value, boundServiceFrameIds(config))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { handles, loading, load, liveServiceEnvUrls }
|
|
52
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { AddPackageRegistryInput, PackageRegistryEntryView } from '~/types/packageRegistries'
|
|
4
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
5
|
+
import { apiErrorStatus } from '~/composables/api/errors'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The workspace's private package-registry entries (npm private orgs, GitHub
|
|
9
|
+
* Packages) that agent containers install with. Tokens are write-only — the store
|
|
10
|
+
* only ever holds the redacted summary views. Loaded on demand (the registries
|
|
11
|
+
* panel + the Integrations hub badge), not from the snapshot.
|
|
12
|
+
*/
|
|
13
|
+
export const usePackageRegistriesStore = defineStore('packageRegistries', () => {
|
|
14
|
+
const api = useApi()
|
|
15
|
+
|
|
16
|
+
const entries = ref<PackageRegistryEntryView[]>([])
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
// Mirrors the backend's opt-in gate (the module 503s when the encryption key is
|
|
19
|
+
// absent): `null` until first probed, then `true`/`false`. The hub hides its
|
|
20
|
+
// registries entry point when this is false.
|
|
21
|
+
const available = ref<boolean | null>(null)
|
|
22
|
+
let inFlight: Promise<void> | null = null
|
|
23
|
+
|
|
24
|
+
/** Force a refresh of the entry list (used after an add/remove). */
|
|
25
|
+
async function load() {
|
|
26
|
+
const ws = useWorkspaceStore()
|
|
27
|
+
loading.value = true
|
|
28
|
+
try {
|
|
29
|
+
entries.value = (await api.listPackageRegistries(ws.requireId())).entries
|
|
30
|
+
available.value = true
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (apiErrorStatus(err) === 503) {
|
|
33
|
+
// A definitive 503 means the integration is unconfigured (no encryption key on
|
|
34
|
+
// the backend): hide the UI entry points and stop probing.
|
|
35
|
+
available.value = false
|
|
36
|
+
entries.value = []
|
|
37
|
+
}
|
|
38
|
+
// Any other failure (transient 5xx / network) is left untouched: it must not hide
|
|
39
|
+
// an already-available panel nor cache a false "unavailable". `available` stays
|
|
40
|
+
// `null` when never probed, so `ensureLoaded` remains retryable on the next open.
|
|
41
|
+
} finally {
|
|
42
|
+
loading.value = false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Load once and share the result (coalescing concurrent callers); `load()` refreshes. */
|
|
47
|
+
async function ensureLoaded() {
|
|
48
|
+
if (available.value !== null) return
|
|
49
|
+
if (!inFlight) inFlight = load().finally(() => (inFlight = null))
|
|
50
|
+
return inFlight
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function add(input: AddPackageRegistryInput) {
|
|
54
|
+
const ws = useWorkspaceStore()
|
|
55
|
+
entries.value = (await api.addPackageRegistry(ws.requireId(), input)).entries
|
|
56
|
+
available.value = true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function remove(entryId: string) {
|
|
60
|
+
const ws = useWorkspaceStore()
|
|
61
|
+
await api.deletePackageRegistry(ws.requireId(), entryId)
|
|
62
|
+
entries.value = entries.value.filter((entry) => entry.id !== entryId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { entries, loading, available, load, ensureLoaded, add, remove }
|
|
66
|
+
})
|
package/app/stores/ui.ts
CHANGED
|
@@ -153,6 +153,9 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
153
153
|
// today, pluggable). NB: distinct from `observabilityInstanceId` below, which is the
|
|
154
154
|
// LLM per-call observability panel.
|
|
155
155
|
const observabilityConnectionOpen = ref(false)
|
|
156
|
+
// Private package registries: the workspace's npm/GitHub-Packages entries agent
|
|
157
|
+
// containers install with. Opened from the Integrations hub.
|
|
158
|
+
const packageRegistriesOpen = ref(false)
|
|
156
159
|
// The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
|
|
157
160
|
// reached via the Integrations hub). Two topical tabs: "Agent containers" (the execution
|
|
158
161
|
// backend + self-hosted runner pool, plus the local-mode warm pool/checkout) and "Test
|
|
@@ -561,6 +564,13 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
561
564
|
function closeObservabilityConnection() {
|
|
562
565
|
observabilityConnectionOpen.value = false
|
|
563
566
|
}
|
|
567
|
+
function openPackageRegistries() {
|
|
568
|
+
resetHubReturn()
|
|
569
|
+
packageRegistriesOpen.value = true
|
|
570
|
+
}
|
|
571
|
+
function closePackageRegistries() {
|
|
572
|
+
packageRegistriesOpen.value = false
|
|
573
|
+
}
|
|
564
574
|
// Top-level navbar entry into the Infrastructure window. No hub-return marker (it isn't
|
|
565
575
|
// reached from the Integrations hub), so the window shows no "Back to Integrations" control.
|
|
566
576
|
function openInfrastructure(tab: 'environment' | 'runner-pool' = 'runner-pool') {
|
|
@@ -791,6 +801,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
791
801
|
accountSettingsTab,
|
|
792
802
|
accountSettingsScrollTarget,
|
|
793
803
|
observabilityConnectionOpen,
|
|
804
|
+
packageRegistriesOpen,
|
|
794
805
|
infrastructureOpen,
|
|
795
806
|
infrastructureTab,
|
|
796
807
|
openInfrastructure,
|
|
@@ -880,6 +891,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
880
891
|
setAccountSettingsTab,
|
|
881
892
|
openObservabilityConnection,
|
|
882
893
|
closeObservabilityConnection,
|
|
894
|
+
openPackageRegistries,
|
|
895
|
+
closePackageRegistries,
|
|
883
896
|
openProviderConnection,
|
|
884
897
|
closeProviderConnection,
|
|
885
898
|
k3sSetupPrefill,
|
package/app/types/domain.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Private package-registry settings shapes. Per-workspace registry entries (npm
|
|
2
|
+
// private orgs, GitHub Packages) whose tokens are write-only — the list view only
|
|
3
|
+
// ever carries the non-secret summary (vendor + scopes + token tail).
|
|
4
|
+
//
|
|
5
|
+
// All wire shapes are sourced from @cat-factory/contracts (single source of truth).
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
PackageEcosystem,
|
|
9
|
+
PackageRegistryVendor,
|
|
10
|
+
AddPackageRegistryInput,
|
|
11
|
+
PackageRegistryEntryView,
|
|
12
|
+
PackageRegistryListView,
|
|
13
|
+
} from '@cat-factory/contracts'
|
package/i18n/locales/en.json
CHANGED
|
@@ -544,6 +544,12 @@
|
|
|
544
544
|
"failed": "Failed",
|
|
545
545
|
"stopped": "Not running"
|
|
546
546
|
}
|
|
547
|
+
},
|
|
548
|
+
"resolved": {
|
|
549
|
+
"title": "Resolves to",
|
|
550
|
+
"duplicateWarning": "Used on more than one binding, so only the last applies: {vars}.",
|
|
551
|
+
"serviceOffline": "{service}: no live environment (mocked)",
|
|
552
|
+
"mock": "Mock (WireMock)"
|
|
547
553
|
}
|
|
548
554
|
},
|
|
549
555
|
"serviceConnections": {
|
|
@@ -1491,6 +1497,7 @@
|
|
|
1491
1497
|
"documents": "Documents",
|
|
1492
1498
|
"taskTrackers": "Task trackers",
|
|
1493
1499
|
"observability": "Observability",
|
|
1500
|
+
"development": "Development",
|
|
1494
1501
|
"personal": "Personal (only you)"
|
|
1495
1502
|
},
|
|
1496
1503
|
"items": {
|
|
@@ -1528,6 +1535,10 @@
|
|
|
1528
1535
|
"label": "Post-release health",
|
|
1529
1536
|
"description": "Watch monitors and SLOs after a release ships (Datadog)."
|
|
1530
1537
|
},
|
|
1538
|
+
"packageRegistries": {
|
|
1539
|
+
"label": "Private package registries",
|
|
1540
|
+
"description": "npm and GitHub Packages tokens agents use to install private dependencies."
|
|
1541
|
+
},
|
|
1531
1542
|
"githubPat": {
|
|
1532
1543
|
"label": "My GitHub token",
|
|
1533
1544
|
"description": "A personal access token used for runs you start (pushes, PRs, CI, merge)."
|
|
@@ -2025,6 +2036,31 @@
|
|
|
2025
2036
|
"connectionNoun": "the observability connection",
|
|
2026
2037
|
"incidentNoun": "the incident provider"
|
|
2027
2038
|
},
|
|
2039
|
+
"packageRegistries": {
|
|
2040
|
+
"title": "Private package registries",
|
|
2041
|
+
"intro": "Connect the private registries your repositories install from. Agents receive them when they check out a repository, so private dependencies resolve during installs. Tokens are write-only: to change one, remove the entry and add it again.",
|
|
2042
|
+
"entryNoun": "registry entry",
|
|
2043
|
+
"list": {
|
|
2044
|
+
"heading": "Connected registries",
|
|
2045
|
+
"tokenTail": "token …{tail}",
|
|
2046
|
+
"remove": "Remove entry"
|
|
2047
|
+
},
|
|
2048
|
+
"add": {
|
|
2049
|
+
"heading": "Add a registry",
|
|
2050
|
+
"vendor": "Registry",
|
|
2051
|
+
"host": "The token is sent only to {host}.",
|
|
2052
|
+
"scopes": "Package scopes",
|
|
2053
|
+
"scopesHelp": "Comma-separated npm scopes",
|
|
2054
|
+
"token": "Access token",
|
|
2055
|
+
"save": "Add registry"
|
|
2056
|
+
},
|
|
2057
|
+
"toast": {
|
|
2058
|
+
"loadFailed": "Could not load package registries",
|
|
2059
|
+
"added": "Registry added",
|
|
2060
|
+
"addFailed": "Could not add the registry",
|
|
2061
|
+
"removeFailed": "Could not remove the registry entry"
|
|
2062
|
+
}
|
|
2063
|
+
},
|
|
2028
2064
|
"localMode": {
|
|
2029
2065
|
"title": "Local mode",
|
|
2030
2066
|
"intro": "Tuning for the local container runner, stored on this machine's deployment (it replaced the {poolVars} / {harnessVars} env vars). Saving resizes the warm pool live, no restart needed; in-flight runs keep the container they already hold.",
|
|
@@ -2945,6 +2981,8 @@
|
|
|
2945
2981
|
},
|
|
2946
2982
|
"attemptsHeading": "{helper} attempts",
|
|
2947
2983
|
"attempt": "Attempt {number}",
|
|
2984
|
+
"attemptInstructions": "Handed to {helper}",
|
|
2985
|
+
"attemptReport": "{helper} report",
|
|
2948
2986
|
"outcome": {
|
|
2949
2987
|
"completed": "completed",
|
|
2950
2988
|
"failed": "failed"
|