@cat-factory/app 0.97.0 → 0.98.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.
@@ -12,6 +12,7 @@ import type {
12
12
  } from '~/types/domain'
13
13
  import FrontendBindingsResolved from '~/components/panels/inspector/FrontendBindingsResolved.vue'
14
14
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
15
+ import { apiErrorEnvelope } from '~/composables/api/errors'
15
16
 
16
17
  // Frontend-frame (`type: 'frontend'`) configuration: how to build, serve, and mock this
17
18
  // frontend for a self-contained UI test (+ an optional browsable preview on local/node),
@@ -119,7 +120,9 @@ const showPreview = ref(false)
119
120
  // frame links to its repo the same way a service does (`github_repos.block_id`).
120
121
  const repoLink = computed(() => github.repoForBlock(props.block.id))
121
122
  const detecting = ref(false)
122
- const detectError = ref(false)
123
+ // The detect failure message to show, or null when there's no error. Holds the SERVER's real
124
+ // message (the backend raises an actionable one for an unreadable repo) instead of a fixed line.
125
+ const detectError = ref<string | null>(null)
123
126
  const detectResult = ref<FrontendConfigRecommendation | null>(null)
124
127
 
125
128
  // A detection result is scoped to the inspected block — clear it (and any error) when the
@@ -128,18 +131,20 @@ watch(
128
131
  () => props.block.id,
129
132
  () => {
130
133
  detectResult.value = null
131
- detectError.value = false
134
+ detectError.value = null
132
135
  },
133
136
  )
134
137
 
135
138
  async function detectFromRepo() {
136
139
  const repo = repoLink.value
137
140
  if (!repo) {
138
- detectError.value = true
141
+ // The frame points at a repo that isn't in the connected-repo projection, so we can't resolve
142
+ // its owner/name to ask the backend. A "sync/connect GitHub" problem, not a read failure.
143
+ detectError.value = t('inspector.detectRepoUnresolved')
139
144
  return
140
145
  }
141
146
  detecting.value = true
142
- detectError.value = false
147
+ detectError.value = null
143
148
  detectResult.value = null
144
149
  try {
145
150
  const result = await infra.detectFrontendConfig({
@@ -151,8 +156,13 @@ async function detectFromRepo() {
151
156
  // When nothing was detected the "none" hint tells the user to set the frontend directory —
152
157
  // that field lives in the (collapsed) Build group, so open it so the advice is actionable.
153
158
  if (!result.detected) showBuild.value = true
154
- } catch {
155
- detectError.value = true
159
+ } catch (e) {
160
+ // Surface the server's real message (an actionable "couldn't read the repo — check App access"
161
+ // for a read fault), falling back to the generic line only when none is available.
162
+ detectError.value =
163
+ apiErrorEnvelope(e)?.message ??
164
+ (e instanceof Error ? e.message : null) ??
165
+ t('inspector.frontendConfig.detect.error')
156
166
  } finally {
157
167
  detecting.value = false
158
168
  }
@@ -243,7 +253,7 @@ onUnmounted(() => preview.stopPolling(props.block.id))
243
253
  </p>
244
254
 
245
255
  <p v-if="detectError" class="text-[11px] text-rose-300/80">
246
- {{ t('inspector.frontendConfig.detect.error') }}
256
+ {{ detectError }}
247
257
  </p>
248
258
 
249
259
  <template v-if="detectResult && !detecting">
@@ -18,6 +18,7 @@ import type {
18
18
  } from '@cat-factory/contracts'
19
19
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
20
20
  import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
21
+ import { apiErrorEnvelope } from '~/composables/api/errors'
21
22
 
22
23
  // Service-level (frame) configuration: the service-owned PROVISIONING — the provision
23
24
  // TYPE this service produces (`infraless` / `docker-compose` / `kubernetes` / `custom`)
@@ -42,6 +43,7 @@ const github = useGitHubStore()
42
43
  const services = useServicesStore()
43
44
  const infra = useInfraConfigStore()
44
45
  const agentRuns = useAgentRunsStore()
46
+ const ui = useUiStore()
45
47
  const { t } = useI18n()
46
48
 
47
49
  // The custom-manifest-type catalog feeds the `custom` picker. Cheap + shared (coalesced).
@@ -284,7 +286,10 @@ function applyPicked() {
284
286
  // every field stays editable, and the engine-level URL/namespace suggestions are surfaced
285
287
  // read-only (the workspace handler owns them). Nothing is persisted server-side by detection.
286
288
  const detecting = ref(false)
287
- const detectError = ref(false)
289
+ // The detect failure message to show, or null when there's no error. Holds the SERVER's real
290
+ // message (the backend now raises an actionable one for an unreadable repo) so the user sees why
291
+ // detection failed instead of a fixed, vague line.
292
+ const detectError = ref<string | null>(null)
288
293
  const detectResult = ref<ProvisioningRecommendation | null>(null)
289
294
  // Advisory, LOCAL-ONLY selection: which compose `services:` key the user picked. It is NOT persisted
290
295
  // (the compose backend targets the file, not a single service), so it lives only in component state
@@ -298,7 +303,7 @@ watch(
298
303
  () => props.block.id,
299
304
  () => {
300
305
  detectResult.value = null
301
- detectError.value = false
306
+ detectError.value = null
302
307
  pickedComposeService.value = null
303
308
  },
304
309
  )
@@ -308,11 +313,14 @@ async function detectFromRepo() {
308
313
  if (!ctx) return
309
314
  const repo = github.repoFor(ctx.githubId)
310
315
  if (!repo) {
311
- detectError.value = true
316
+ // The frame points at a repo that isn't in the connected-repo projection, so we can't resolve
317
+ // its owner/name to ask the backend. That's a "sync/connect GitHub" problem, NOT "couldn't read
318
+ // the repo" — say so specifically.
319
+ detectError.value = t('inspector.detectRepoUnresolved')
312
320
  return
313
321
  }
314
322
  detecting.value = true
315
- detectError.value = false
323
+ detectError.value = null
316
324
  try {
317
325
  const rec = await infra.detectProvisioning({
318
326
  owner: repo.owner,
@@ -342,8 +350,13 @@ async function detectFromRepo() {
342
350
  board.updateBlock(props.block.id, { provisioning: rec.provisioning })
343
351
  if (rec.provisioning.type === 'kubernetes') seedKubeSource(rec.provisioning.manifestSource)
344
352
  }
345
- } catch {
346
- detectError.value = true
353
+ } catch (e) {
354
+ // Surface the server's real message (an actionable "couldn't read the repo — check App access"
355
+ // for a read fault), falling back to the generic line only when none is available.
356
+ detectError.value =
357
+ apiErrorEnvelope(e)?.message ??
358
+ (e instanceof Error ? e.message : null) ??
359
+ t('inspector.testConfig.detect.error')
347
360
  } finally {
348
361
  detecting.value = false
349
362
  }
@@ -431,6 +444,34 @@ function setSize(value: InstanceSize) {
431
444
  </p>
432
445
  </div>
433
446
 
447
+ <!-- Nudge into the guided environment setup wizard for a docker-compose service: the wizard
448
+ drives detect → review (recipe + analyst draft) → preflight → save so the single Deployer
449
+ provisions the compose stack, rather than editing the raw path inline. -->
450
+ <div
451
+ v-if="provisionType === 'docker-compose'"
452
+ class="flex items-center justify-between gap-2 rounded border border-primary-800/40 bg-primary-950/20 p-2"
453
+ data-testid="env-setup-nudge"
454
+ >
455
+ <div class="min-w-0">
456
+ <p class="text-[11px] font-medium text-primary-200/90">
457
+ {{ t('inspector.testConfig.envWizard.title') }}
458
+ </p>
459
+ <p class="text-[11px] leading-snug text-slate-500">
460
+ {{ t('inspector.testConfig.envWizard.hint') }}
461
+ </p>
462
+ </div>
463
+ <UButton
464
+ size="xs"
465
+ variant="soft"
466
+ color="primary"
467
+ icon="i-lucide-flask-conical"
468
+ data-testid="env-setup-nudge-open"
469
+ @click="ui.openEnvironmentSetup(props.block.id)"
470
+ >
471
+ {{ t('inspector.testConfig.envWizard.open') }}
472
+ </UButton>
473
+ </div>
474
+
434
475
  <!-- Auto-detect a recommended provisioning config from the repo (slice 11). Non-binding:
435
476
  it prefills the form below + the kube edit refs; the user confirms/edits everything. -->
436
477
  <div v-if="repoContext" class="space-y-2 rounded border border-slate-800 bg-slate-900/40 p-2">
@@ -452,7 +493,7 @@ function setSize(value: InstanceSize) {
452
493
  </p>
453
494
 
454
495
  <p v-if="detectError" class="text-[11px] text-rose-300/80">
455
- {{ t('inspector.testConfig.detect.error') }}
496
+ {{ detectError }}
456
497
  </p>
457
498
 
458
499
  <template v-if="detectResult && !detecting">
@@ -1,4 +1,5 @@
1
- import { listEnvironmentsContract } from '@cat-factory/contracts'
1
+ import { listEnvironmentsContract, provisionEnvironmentContract } from '@cat-factory/contracts'
2
+ import type { ProvisionEnvironmentInput } from '@cat-factory/contracts'
2
3
  import type { ApiContext } from './context'
3
4
 
4
5
  /** Ephemeral environments: the workspace's live env handles (used to resolve frontend bindings). */
@@ -6,5 +7,10 @@ export function environmentsApi({ send, ws }: ApiContext) {
6
7
  return {
7
8
  listEnvironments: (workspaceId: string) =>
8
9
  send(listEnvironmentsContract, { pathPrefix: ws(workspaceId) }),
10
+
11
+ // Manually provision an environment for a service frame (outside a pipeline run) — the setup
12
+ // wizard's "trial provision" against the just-saved config. Returns the resulting handle.
13
+ provisionEnvironment: (workspaceId: string, body: ProvisionEnvironmentInput) =>
14
+ send(provisionEnvironmentContract, { pathPrefix: ws(workspaceId), body }),
9
15
  }
10
16
  }
@@ -0,0 +1,16 @@
1
+ import { runPreflightsContract } from '@cat-factory/contracts'
2
+ import type { PreflightRef } from '@cat-factory/contracts'
3
+ import type { ApiContext } from './context'
4
+
5
+ /**
6
+ * Preflight checks (machine-prerequisite probes for a stack recipe): run a set of refs and get one
7
+ * verdict each (pass / fail / warn + detail + remediation). Used by the environment setup wizard's
8
+ * checklist + live re-check. The probes run only on the local (host) facade; the endpoint 503s
9
+ * elsewhere. See PreflightController in @cat-factory/server.
10
+ */
11
+ export function preflightsApi({ send, ws }: ApiContext) {
12
+ return {
13
+ runPreflights: (workspaceId: string, prerequisites: PreflightRef[]) =>
14
+ send(runPreflightsContract, { pathPrefix: ws(workspaceId), body: { prerequisites } }),
15
+ }
16
+ }
@@ -21,6 +21,7 @@ import { localSettingsApi } from './api/localSettings'
21
21
  import { modelsApi } from './api/models'
22
22
  import { notificationsApi } from './api/notifications'
23
23
  import { packageRegistriesApi } from './api/packageRegistries'
24
+ import { preflightsApi } from './api/preflights'
24
25
  import { presetsApi } from './api/presets'
25
26
  import { sharedStacksApi } from './api/sharedStacks'
26
27
  import { providerConnectionsApi } from './api/providerConnections'
@@ -113,6 +114,7 @@ export function useApi() {
113
114
  ...specApi(ctx),
114
115
  ...notificationsApi(ctx),
115
116
  ...presetsApi(ctx),
117
+ ...preflightsApi(ctx),
116
118
  ...sharedStacksApi(ctx),
117
119
  ...providerConnectionsApi(ctx),
118
120
  ...infraHandlersApi(ctx),
@@ -95,6 +95,9 @@ const PackageRegistriesPanel = defineAsyncComponent(
95
95
  const InfrastructureWindow = defineAsyncComponent(
96
96
  () => import('~/components/settings/InfrastructureWindow.vue'),
97
97
  )
98
+ const EnvironmentSetupWizard = defineAsyncComponent(
99
+ () => import('~/components/environments/EnvironmentSetupWizard.vue'),
100
+ )
98
101
  const ModelConfigurationPanel = defineAsyncComponent(
99
102
  () => import('~/components/settings/ModelConfigurationPanel.vue'),
100
103
  )
@@ -373,6 +376,7 @@ watch(
373
376
  <ObservabilityConnectionPanel v-if="ui.observabilityConnectionOpen" />
374
377
  <PackageRegistriesPanel v-if="ui.packageRegistriesOpen" />
375
378
  <InfrastructureWindow v-if="ui.infrastructureOpen" />
379
+ <EnvironmentSetupWizard v-if="ui.environmentWizardOpen" />
376
380
  <ModelConfigurationPanel v-if="ui.modelConfigOpen" />
377
381
  <LocalModelEndpointsPanel v-if="ui.localModelsOpen" />
378
382
  <SandboxPanel v-if="ui.sandboxOpen" />