@cat-factory/app 0.60.3 → 0.61.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.
@@ -7,7 +7,12 @@ import type {
7
7
  ProvisionType,
8
8
  ServiceProvisioning,
9
9
  } from '~/types/domain'
10
- import type { KubernetesManifestSource, KubernetesRenderer } from '@cat-factory/contracts'
10
+ import type {
11
+ KubernetesManifestSource,
12
+ KubernetesRenderer,
13
+ ProvisioningOverlayCandidate,
14
+ ProvisioningRecommendation,
15
+ } from '@cat-factory/contracts'
11
16
  import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
12
17
 
13
18
  // Service-level (frame) configuration: the service-owned PROVISIONING — the provision
@@ -32,7 +37,11 @@ const infra = useInfraConfigStore()
32
37
  const { t } = useI18n()
33
38
 
34
39
  // The custom-manifest-type catalog feeds the `custom` picker. Cheap + shared (coalesced).
35
- onMounted(() => void infra.ensureLoaded())
40
+ // The repo list backs the detect-from-repo affordance (owner/name lookup).
41
+ onMounted(() => {
42
+ void infra.ensureLoaded()
43
+ void github.ensureLoaded()
44
+ })
36
45
 
37
46
  // The service's declared provision type (absent ⇒ treated as `infraless`: no environment
38
47
  // is stood up for the Tester). Switching type MERGES onto the existing provisioning so each
@@ -52,17 +61,22 @@ const kubeRepo = ref('')
52
61
  const kubeRef = ref('')
53
62
  const kubePath = ref('')
54
63
  const kubeRenderer = ref<KubernetesRenderer>('raw')
64
+ // Seed the local kube edit refs from a persisted manifest source. Reused by the per-block
65
+ // watch AND after a detect-from-repo run (which mutates provisioning without changing block.id,
66
+ // so the watch wouldn't re-fire on its own).
67
+ function seedKubeSource(src?: KubernetesManifestSource) {
68
+ kubeSourceType.value = src?.type ?? 'colocated'
69
+ kubePath.value = src?.path ?? ''
70
+ kubeRenderer.value = src?.renderer ?? 'raw'
71
+ kubeRepo.value = src?.type === 'separate' ? src.repo : ''
72
+ kubeRef.value = src?.type === 'separate' ? (src.ref ?? '') : ''
73
+ }
55
74
  watch(
56
75
  () => props.block.id,
57
- () => {
58
- const src = props.block.provisioning?.manifestSource
59
- kubeSourceType.value = src?.type ?? 'colocated'
60
- kubePath.value = src?.path ?? ''
61
- kubeRenderer.value = src?.renderer ?? 'raw'
62
- kubeRepo.value = src?.type === 'separate' ? src.repo : ''
63
- kubeRef.value = src?.type === 'separate' ? (src.ref ?? '') : ''
76
+ () => seedKubeSource(props.block.provisioning?.manifestSource),
77
+ {
78
+ immediate: true,
64
79
  },
65
- { immediate: true },
66
80
  )
67
81
  const customManifestId = computed(() => props.block.provisioning?.manifestId ?? '')
68
82
  const customManifestPath = computed(() => props.block.provisioning?.manifestPath ?? '')
@@ -183,6 +197,65 @@ function applyPicked() {
183
197
  browseOpen.value = false
184
198
  }
185
199
 
200
+ // Auto-detect (slice 11): read the repo checkout-free and propose a NON-BINDING provisioning
201
+ // config. The user always confirms — the result prefills the form (and the kube edit refs) but
202
+ // every field stays editable, and the engine-level URL/namespace suggestions are surfaced
203
+ // read-only (the workspace handler owns them). Nothing is persisted server-side by detection.
204
+ const detecting = ref(false)
205
+ const detectError = ref(false)
206
+ const detectResult = ref<ProvisioningRecommendation | null>(null)
207
+
208
+ // A detection result is scoped to the inspected block — clear it (and any error) when the
209
+ // selection changes, so block B never shows block A's stale recommendation / overlay chips.
210
+ watch(
211
+ () => props.block.id,
212
+ () => {
213
+ detectResult.value = null
214
+ detectError.value = false
215
+ },
216
+ )
217
+
218
+ async function detectFromRepo() {
219
+ const ctx = repoContext.value
220
+ if (!ctx) return
221
+ const repo = github.repoFor(ctx.githubId)
222
+ if (!repo) {
223
+ detectError.value = true
224
+ return
225
+ }
226
+ detecting.value = true
227
+ detectError.value = false
228
+ try {
229
+ const rec = await infra.detectProvisioning({
230
+ owner: repo.owner,
231
+ repo: repo.name,
232
+ ...(ctx.directory ? { directory: ctx.directory } : {}),
233
+ })
234
+ detectResult.value = rec
235
+ // Only prefill when the detector actually inferred something. A `detected: false`
236
+ // recommendation is `infraless`; applying it would WIPE the service's existing
237
+ // provisioning (board.updateBlock persists immediately). Leave the current config
238
+ // untouched and just surface the "nothing found" note.
239
+ if (rec.detected) {
240
+ board.updateBlock(props.block.id, { provisioning: rec.provisioning })
241
+ if (rec.provisioning.type === 'kubernetes') seedKubeSource(rec.provisioning.manifestSource)
242
+ }
243
+ } catch {
244
+ detectError.value = true
245
+ } finally {
246
+ detecting.value = false
247
+ }
248
+ }
249
+
250
+ // Switch the recommended manifest path to a different overlay candidate (the user's pick).
251
+ function applyOverlay(candidate: ProvisioningOverlayCandidate) {
252
+ setKubePath(candidate.path)
253
+ }
254
+
255
+ function provisionTypeLabel(type: ProvisionType): string {
256
+ return t(`inspector.testConfig.provisionTypes.${type}`)
257
+ }
258
+
186
259
  // A service with no explicit provider inherits the active account's default (else the
187
260
  // built-in `cloudflare`); show that as the selected chip so the inherited value is visible.
188
261
  const effectiveProvider = computed<CloudProvider>(
@@ -237,6 +310,90 @@ function setSize(value: InstanceSize) {
237
310
  </p>
238
311
  </div>
239
312
 
313
+ <!-- Auto-detect a recommended provisioning config from the repo (slice 11). Non-binding:
314
+ it prefills the form below + the kube edit refs; the user confirms/edits everything. -->
315
+ <div v-if="repoContext" class="space-y-2 rounded border border-slate-800 bg-slate-900/40 p-2">
316
+ <div class="flex items-center justify-between gap-2">
317
+ <span class="text-[11px] text-slate-400">{{ t('inspector.testConfig.detect.title') }}</span>
318
+ <UButton
319
+ size="xs"
320
+ variant="soft"
321
+ color="primary"
322
+ icon="i-lucide-wand-sparkles"
323
+ :loading="detecting"
324
+ @click="detectFromRepo"
325
+ >
326
+ {{ t('inspector.testConfig.detect.button') }}
327
+ </UButton>
328
+ </div>
329
+ <p class="text-[11px] leading-snug text-slate-500">
330
+ {{ t('inspector.testConfig.detect.hint') }}
331
+ </p>
332
+
333
+ <p v-if="detectError" class="text-[11px] text-rose-300/80">
334
+ {{ t('inspector.testConfig.detect.error') }}
335
+ </p>
336
+
337
+ <template v-if="detectResult && !detecting">
338
+ <p v-if="!detectResult.detected" class="text-[11px] text-amber-300/80">
339
+ {{ t('inspector.testConfig.detect.none') }}
340
+ </p>
341
+ <template v-else>
342
+ <p class="text-[11px] text-emerald-300/80">
343
+ {{
344
+ t('inspector.testConfig.detect.applied', {
345
+ type: provisionTypeLabel(detectResult.provisioning.type),
346
+ })
347
+ }}
348
+ </p>
349
+
350
+ <div v-if="detectResult.overlayCandidates?.length" class="space-y-1">
351
+ <span class="text-[11px] text-slate-400">{{
352
+ t('inspector.testConfig.detect.overlayTitle')
353
+ }}</span>
354
+ <div class="flex flex-wrap gap-1">
355
+ <UButton
356
+ v-for="o in detectResult.overlayCandidates"
357
+ :key="o.path"
358
+ :color="kubePath === o.path ? 'primary' : 'neutral'"
359
+ :variant="kubePath === o.path ? 'soft' : 'ghost'"
360
+ size="xs"
361
+ @click="applyOverlay(o)"
362
+ >
363
+ {{ o.name }}
364
+ </UButton>
365
+ </div>
366
+ </div>
367
+
368
+ <p v-if="detectResult.urlSource" class="text-[11px] text-slate-500">
369
+ {{
370
+ t('inspector.testConfig.detect.urlSource', { source: detectResult.urlSource.source })
371
+ }}
372
+ </p>
373
+ <p v-if="detectResult.namespace" class="text-[11px] text-slate-500">
374
+ {{ t('inspector.testConfig.detect.namespace', { namespace: detectResult.namespace }) }}
375
+ </p>
376
+
377
+ <ul v-if="detectResult.notes.length" class="space-y-0.5">
378
+ <li
379
+ v-for="(n, i) in detectResult.notes"
380
+ :key="i"
381
+ class="flex items-start gap-1.5 text-[11px] leading-snug text-slate-500"
382
+ >
383
+ <span :class="n.confidence === 'high' ? 'text-emerald-400/70' : 'text-amber-400/70'">
384
+ {{
385
+ n.confidence === 'high'
386
+ ? t('inspector.testConfig.detect.confidenceHigh')
387
+ : t('inspector.testConfig.detect.confidenceLow')
388
+ }}
389
+ </span>
390
+ <span>{{ n.message }}</span>
391
+ </li>
392
+ </ul>
393
+ </template>
394
+ </template>
395
+ </div>
396
+
240
397
  <div v-if="provisionType === 'docker-compose'" class="space-y-2">
241
398
  <div class="space-y-1">
242
399
  <label class="text-[11px] text-slate-400">{{
@@ -1,4 +1,5 @@
1
1
  import {
2
+ detectServiceProvisioningContract,
2
3
  listEnvironmentHandlersContract,
3
4
  listEnvironmentUserHandlersContract,
4
5
  registerEnvironmentHandlerContract,
@@ -9,6 +10,7 @@ import {
9
10
  upsertEnvironmentUserHandlerContract,
10
11
  } from '@cat-factory/contracts'
11
12
  import type {
13
+ DetectServiceProvisioningInput,
12
14
  ProvisionType,
13
15
  RegisterEnvironmentHandlerInput,
14
16
  UpsertCustomManifestTypeInput,
@@ -33,6 +35,10 @@ export function infraHandlersApi({ send, ws }: ApiContext) {
33
35
  registerEnvironmentHandler: (workspaceId: string, body: RegisterEnvironmentHandlerInput) =>
34
36
  send(registerEnvironmentHandlerContract, { pathPrefix: ws(workspaceId), body }),
35
37
 
38
+ // Auto-detect a non-binding recommended provisioning config from a service's repo.
39
+ detectServiceProvisioning: (workspaceId: string, body: DetectServiceProvisioningInput) =>
40
+ send(detectServiceProvisioningContract, { pathPrefix: ws(workspaceId), body }),
41
+
36
42
  // `manifestId` (for a `custom` handler) rides as a query param; absent ⇒ the bare handler.
37
43
  unregisterEnvironmentHandler: (
38
44
  workspaceId: string,
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
2
2
  import { ref, type Ref } from 'vue'
3
3
  import type {
4
4
  CustomManifestType,
5
+ DetectServiceProvisioningInput,
5
6
  EnvironmentHandlerView,
6
7
  ProvisionType,
7
8
  RegisterEnvironmentHandlerInput,
@@ -94,6 +95,16 @@ export const useInfraConfigStore = defineStore('infraConfig', () => {
94
95
  return saved
95
96
  }
96
97
 
98
+ /**
99
+ * Auto-detect a NON-BINDING recommended provisioning config from a service's repo. The SPA
100
+ * prefills the confirm form from the result; nothing is persisted server-side. Detection is
101
+ * pure repo introspection, so it works regardless of which handlers are registered.
102
+ */
103
+ async function detectProvisioning(input: DetectServiceProvisioningInput) {
104
+ const ws = useWorkspaceStore()
105
+ return api.detectServiceProvisioning(ws.requireId(), input)
106
+ }
107
+
97
108
  async function unregisterHandler(type: ProvisionType, manifestId?: string | null) {
98
109
  const ws = useWorkspaceStore()
99
110
  await api.unregisterEnvironmentHandler(ws.requireId(), type, manifestId ?? undefined)
@@ -161,6 +172,7 @@ export const useInfraConfigStore = defineStore('infraConfig', () => {
161
172
  ensureLoaded,
162
173
  handlerFor,
163
174
  registerHandler,
175
+ detectProvisioning,
164
176
  unregisterHandler,
165
177
  upsertCustomType,
166
178
  removeCustomType,
@@ -474,7 +474,20 @@
474
474
  "customManifestIdPlaceholder": "Pick a manifest type",
475
475
  "customNoTypes": "No custom manifest types are defined yet. Add one in the Infrastructure window.",
476
476
  "customManifestIdHint": "The custom type this service produces, matched to a remote-custom handler the workspace configures.",
477
- "customManifestPath": "Manifest path (optional)"
477
+ "customManifestPath": "Manifest path (optional)",
478
+ "detect": {
479
+ "title": "Auto-detect",
480
+ "button": "Detect from repo",
481
+ "hint": "Inspect the repository and suggest a provisioning config. You confirm and edit everything below; nothing is saved automatically.",
482
+ "error": "Could not read the repository to detect provisioning.",
483
+ "none": "No Kubernetes manifests or Compose file were detected.",
484
+ "applied": "Suggested a {type} config. Review and adjust the fields below.",
485
+ "overlayTitle": "Ephemeral overlay",
486
+ "urlSource": "Suggested environment URL source: {source}. The workspace handler owns this; set it there.",
487
+ "namespace": "Manifests pin namespace \"{namespace}\"; recommend honoring it on the workspace handler.",
488
+ "confidenceHigh": "Detected",
489
+ "confidenceLow": "Suggestion"
490
+ }
478
491
  },
479
492
  "agentConfig": {
480
493
  "title": "Agent configuration",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "Elige un tipo de manifiesto",
438
438
  "customNoTypes": "Aún no hay tipos de manifiesto personalizados. Añade uno en la ventana de Infraestructura.",
439
439
  "customManifestIdHint": "El tipo personalizado que produce este servicio, emparejado con un gestor remote-custom que configura el espacio de trabajo.",
440
- "customManifestPath": "Ruta del manifiesto (opcional)"
440
+ "customManifestPath": "Ruta del manifiesto (opcional)",
441
+ "detect": {
442
+ "title": "Detección automática",
443
+ "button": "Detectar desde el repo",
444
+ "hint": "Inspecciona el repositorio y sugiere una configuración de aprovisionamiento. Confirmas y editas todo lo de abajo; nada se guarda automáticamente.",
445
+ "error": "No se pudo leer el repositorio para detectar el aprovisionamiento.",
446
+ "none": "No se detectaron manifiestos de Kubernetes ni archivo Compose.",
447
+ "applied": "Se sugirió una configuración {type}. Revisa y ajusta los campos de abajo.",
448
+ "overlayTitle": "Overlay efímero",
449
+ "urlSource": "Fuente de URL del entorno sugerida: {source}. El gestor del espacio de trabajo la controla; configúrala allí.",
450
+ "namespace": "Los manifiestos fijan el espacio de nombres \"{namespace}\"; se recomienda respetarlo en el gestor del espacio de trabajo.",
451
+ "confidenceHigh": "Detectado",
452
+ "confidenceLow": "Sugerencia"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "Configuración del agente",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "Choisir un type de manifeste",
438
438
  "customNoTypes": "Aucun type de manifeste personnalisé n'est encore défini. Ajoutez-en un dans la fenêtre Infrastructure.",
439
439
  "customManifestIdHint": "Le type personnalisé que ce service produit, associé à un gestionnaire remote-custom configuré par l'espace de travail.",
440
- "customManifestPath": "Chemin du manifeste (facultatif)"
440
+ "customManifestPath": "Chemin du manifeste (facultatif)",
441
+ "detect": {
442
+ "title": "Détection automatique",
443
+ "button": "Détecter depuis le dépôt",
444
+ "hint": "Inspecte le dépôt et propose une configuration de provisionnement. Vous confirmez et modifiez tout ci-dessous ; rien n'est enregistré automatiquement.",
445
+ "error": "Impossible de lire le dépôt pour détecter le provisionnement.",
446
+ "none": "Aucun manifeste Kubernetes ni fichier Compose détecté.",
447
+ "applied": "Configuration {type} suggérée. Vérifiez et ajustez les champs ci-dessous.",
448
+ "overlayTitle": "Overlay éphémère",
449
+ "urlSource": "Source d'URL d'environnement suggérée : {source}. Le gestionnaire de l'espace de travail la contrôle ; définissez-la là.",
450
+ "namespace": "Les manifestes fixent l'espace de noms « {namespace} » ; il est recommandé de le respecter sur le gestionnaire de l'espace de travail.",
451
+ "confidenceHigh": "Détecté",
452
+ "confidenceLow": "Suggestion"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "Configuration de l'agent",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "בחר סוג מניפסט",
438
438
  "customNoTypes": "עדיין לא הוגדרו סוגי מניפסט מותאמים. הוסף אחד בחלון התשתית.",
439
439
  "customManifestIdHint": "הסוג המותאם שהשירות הזה מייצר, המותאם למטפל remote-custom שהמרחב מגדיר.",
440
- "customManifestPath": "נתיב המניפסט (אופציונלי)"
440
+ "customManifestPath": "נתיב המניפסט (אופציונלי)",
441
+ "detect": {
442
+ "title": "זיהוי אוטומטי",
443
+ "button": "זהה מהמאגר",
444
+ "hint": "בדיקת המאגר והצעת תצורת הקצאה. אתה מאשר ועורך הכול למטה; שום דבר לא נשמר אוטומטית.",
445
+ "error": "לא ניתן לקרוא את המאגר כדי לזהות את ההקצאה.",
446
+ "none": "לא זוהו מניפסטים של Kubernetes או קובץ Compose.",
447
+ "applied": "הוצעה תצורת {type}. בדוק והתאם את השדות למטה.",
448
+ "overlayTitle": "שכבת סביבה זמנית",
449
+ "urlSource": "מקור כתובת הסביבה המוצע: {source}. המטפל של המרחב שולט בכך; הגדר זאת שם.",
450
+ "namespace": "המניפסטים מקבעים את מרחב השמות \"{namespace}\"; מומלץ לכבד אותו במטפל של המרחב.",
451
+ "confidenceHigh": "זוהה",
452
+ "confidenceLow": "הצעה"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "תצורת סוכן",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "マニフェストタイプを選択",
438
438
  "customNoTypes": "カスタムマニフェストタイプはまだ定義されていません。インフラウィンドウで追加してください。",
439
439
  "customManifestIdHint": "このサービスが生成するカスタムタイプ。ワークスペースが構成する remote-custom ハンドラーと照合されます。",
440
- "customManifestPath": "マニフェストのパス(任意)"
440
+ "customManifestPath": "マニフェストのパス(任意)",
441
+ "detect": {
442
+ "title": "自動検出",
443
+ "button": "リポジトリから検出",
444
+ "hint": "リポジトリを調べてプロビジョニング設定を提案します。以下の内容はすべて確認・編集でき、自動保存はされません。",
445
+ "error": "リポジトリを読み取ってプロビジョニングを検出できませんでした。",
446
+ "none": "Kubernetes マニフェストや Compose ファイルは検出されませんでした。",
447
+ "applied": "{type} の設定を提案しました。以下のフィールドを確認して調整してください。",
448
+ "overlayTitle": "一時環境のオーバーレイ",
449
+ "urlSource": "推奨される環境 URL ソース: {source}。これはワークスペースのハンドラーが管理します。そちらで設定してください。",
450
+ "namespace": "マニフェストは名前空間「{namespace}」を固定しています。ワークスペースのハンドラーでそれを尊重することを推奨します。",
451
+ "confidenceHigh": "検出",
452
+ "confidenceLow": "提案"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "エージェント設定",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "Wybierz typ manifestu",
438
438
  "customNoTypes": "Nie zdefiniowano jeszcze niestandardowych typów manifestów. Dodaj jeden w oknie Infrastruktura.",
439
439
  "customManifestIdHint": "Niestandardowy typ, który tworzy ta usługa, dopasowany do handlera remote-custom skonfigurowanego przez przestrzeń roboczą.",
440
- "customManifestPath": "Ścieżka manifestu (opcjonalnie)"
440
+ "customManifestPath": "Ścieżka manifestu (opcjonalnie)",
441
+ "detect": {
442
+ "title": "Autowykrywanie",
443
+ "button": "Wykryj z repozytorium",
444
+ "hint": "Sprawdza repozytorium i proponuje konfigurację provisioningu. Potwierdzasz i edytujesz wszystko poniżej; nic nie jest zapisywane automatycznie.",
445
+ "error": "Nie udało się odczytać repozytorium, aby wykryć provisioning.",
446
+ "none": "Nie wykryto manifestów Kubernetes ani pliku Compose.",
447
+ "applied": "Zaproponowano konfigurację {type}. Przejrzyj i dostosuj pola poniżej.",
448
+ "overlayTitle": "Tymczasowy overlay",
449
+ "urlSource": "Sugerowane źródło adresu URL środowiska: {source}. Zarządza tym handler przestrzeni roboczej; ustaw to tam.",
450
+ "namespace": "Manifesty ustalają przestrzeń nazw \"{namespace}\"; zaleca się jej przestrzeganie w handlerze przestrzeni roboczej.",
451
+ "confidenceHigh": "Wykryto",
452
+ "confidenceLow": "Sugestia"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "Konfiguracja agenta",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "Bir manifest türü seçin",
438
438
  "customNoTypes": "Henüz özel manifest türü tanımlanmadı. Altyapı penceresinden bir tane ekleyin.",
439
439
  "customManifestIdHint": "Bu servisin ürettiği özel tür; çalışma alanının yapılandırdığı remote-custom işleyiciyle eşleştirilir.",
440
- "customManifestPath": "Manifest yolu (isteğe bağlı)"
440
+ "customManifestPath": "Manifest yolu (isteğe bağlı)",
441
+ "detect": {
442
+ "title": "Otomatik algılama",
443
+ "button": "Depodan algıla",
444
+ "hint": "Depoyu inceleyip bir provizyon yapılandırması önerir. Aşağıdaki her şeyi onaylayıp düzenlersiniz; hiçbir şey otomatik kaydedilmez.",
445
+ "error": "Provizyonu algılamak için depo okunamadı.",
446
+ "none": "Kubernetes manifesti veya Compose dosyası algılanmadı.",
447
+ "applied": "{type} yapılandırması önerildi. Aşağıdaki alanları gözden geçirip ayarlayın.",
448
+ "overlayTitle": "Geçici overlay",
449
+ "urlSource": "Önerilen ortam URL kaynağı: {source}. Bunu çalışma alanı işleyicisi yönetir; oradan ayarlayın.",
450
+ "namespace": "Manifestler \"{namespace}\" ad alanını sabitliyor; çalışma alanı işleyicisinde buna uymanız önerilir.",
451
+ "confidenceHigh": "Algılandı",
452
+ "confidenceLow": "Öneri"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "Ajan yapılandırması",
@@ -437,7 +437,20 @@
437
437
  "customManifestIdPlaceholder": "Виберіть тип маніфесту",
438
438
  "customNoTypes": "Власні типи маніфестів ще не визначені. Додайте один у вікні Інфраструктура.",
439
439
  "customManifestIdHint": "Власний тип, який створює ця служба, зіставлений з обробником remote-custom, який налаштовує робочий простір.",
440
- "customManifestPath": "Шлях до маніфесту (необов'язково)"
440
+ "customManifestPath": "Шлях до маніфесту (необов'язково)",
441
+ "detect": {
442
+ "title": "Автовизначення",
443
+ "button": "Визначити з репозиторію",
444
+ "hint": "Перевірити репозиторій і запропонувати конфігурацію провізіювання. Ви підтверджуєте та редагуєте все нижче; нічого не зберігається автоматично.",
445
+ "error": "Не вдалося прочитати репозиторій для визначення провізіювання.",
446
+ "none": "Маніфести Kubernetes або файл Compose не виявлено.",
447
+ "applied": "Запропоновано конфігурацію {type}. Перегляньте та скоригуйте поля нижче.",
448
+ "overlayTitle": "Тимчасовий overlay",
449
+ "urlSource": "Запропоноване джерело URL середовища: {source}. Цим керує обробник робочого простору; налаштуйте його там.",
450
+ "namespace": "Маніфести фіксують простір імен \"{namespace}\"; рекомендуємо дотримуватися його в обробнику робочого простору.",
451
+ "confidenceHigh": "Виявлено",
452
+ "confidenceLow": "Пропозиція"
453
+ }
441
454
  },
442
455
  "agentConfig": {
443
456
  "title": "Конфігурація агента",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.60.3",
3
+ "version": "0.61.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.67.0"
37
+ "@cat-factory/contracts": "0.68.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",