@cat-factory/app 0.189.0 → 0.190.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.
@@ -6,6 +6,7 @@
6
6
  // they succeed only on the local facade (elsewhere the backend returns a clear error surfaced as
7
7
  // a toast). Renders inline inside the Infrastructure window's "Shared stacks" tab.
8
8
  import { computed, reactive, ref } from 'vue'
9
+ import { describeComposeSource, normalizeComposeFileRefs } from '@cat-factory/contracts'
9
10
  import type {
10
11
  SharedStack,
11
12
  SharedStackRecommendation,
@@ -76,8 +77,26 @@ function tokens(value: string): string[] {
76
77
  .filter(Boolean)
77
78
  }
78
79
 
80
+ // A stack's compose layers may be bare in-repo paths (what this form authors and what the
81
+ // autodetect scan returns) or explicit sources — an inline document, or a file in another repo —
82
+ // which arrive through the API / a deployment's programmatic seeds. The form edits only the
83
+ // former; a stack carrying any of the latter shows its layers read-only and its save omits
84
+ // `composeFiles` entirely, so editing the name or the profiles can never silently flatten a
85
+ // declaration this form has no editor for.
86
+ const editingStack = computed(() => stacks.value.find((s) => s.id === editingId.value) ?? null)
87
+ const advancedLayers = computed(() =>
88
+ (editingStack.value?.composeFiles ?? []).some((ref) => typeof ref !== 'string'),
89
+ )
90
+ const layerLabels = computed(() =>
91
+ normalizeComposeFileRefs(editingStack.value?.composeFiles ?? []).map(describeComposeSource),
92
+ )
93
+
94
+ // A repo-LESS stack (every layer inline / from another repo) needs no clone URL, so the form
95
+ // requires one only while the layers it can author — in-repo paths — are what is being saved.
79
96
  const canSave = computed(
80
- () => form.name.trim() && form.cloneUrl.trim() && tokens(form.composeFiles).length > 0,
97
+ () =>
98
+ form.name.trim() &&
99
+ (advancedLayers.value || (form.cloneUrl.trim() && tokens(form.composeFiles).length > 0)),
81
100
  )
82
101
 
83
102
  function resetForm() {
@@ -97,10 +116,12 @@ function resetForm() {
97
116
  function startEdit(stack: SharedStack) {
98
117
  editingId.value = stack.id
99
118
  form.name = stack.name
100
- form.cloneUrl = stack.cloneUrl
119
+ form.cloneUrl = stack.cloneUrl ?? ''
101
120
  form.gitRef = stack.gitRef ?? ''
102
121
  form.directory = ''
103
- form.composeFiles = stack.composeFiles.join(', ')
122
+ // Only bare in-repo paths are editable here; a stack with richer layers renders them read-only
123
+ // below and keeps them untouched through the save.
124
+ form.composeFiles = stack.composeFiles.filter((ref) => typeof ref === 'string').join(', ')
104
125
  form.composeProfiles = stack.composeProfiles.join(', ')
105
126
  form.managedNetworks = stack.managedNetworks.join(', ')
106
127
  form.allowHostCommands = stack.allowHostCommands
@@ -179,7 +200,17 @@ async function saveStack() {
179
200
  }
180
201
  try {
181
202
  if (editing) {
182
- await store.update(editing, { ...payload, gitRef: form.gitRef.trim() || null })
203
+ // `composeFiles` is omitted when the stack carries layers this form can't author — the
204
+ // partial update preserves them, exactly as it already does for setup steps and the health
205
+ // gate. `cloneUrl` goes through as an explicit null when cleared, so a stack can be moved to
206
+ // the repo-less shape from here too.
207
+ const { composeFiles, ...rest } = payload
208
+ await store.update(editing, {
209
+ ...rest,
210
+ cloneUrl: form.cloneUrl.trim() || null,
211
+ gitRef: form.gitRef.trim() || null,
212
+ ...(advancedLayers.value ? {} : { composeFiles }),
213
+ })
183
214
  } else {
184
215
  await store.create(payload)
185
216
  }
@@ -402,6 +433,23 @@ async function remove(stack: SharedStack) {
402
433
  </p>
403
434
 
404
435
  <UFormField
436
+ v-if="advancedLayers"
437
+ :label="t('settings.sharedStacks.add.composeFiles')"
438
+ :help="t('settings.sharedStacks.add.composeLayersManagedHelp')"
439
+ >
440
+ <ul class="space-y-1" data-testid="shared-stack-compose-layers">
441
+ <li
442
+ v-for="(label, index) in layerLabels"
443
+ :key="index"
444
+ class="font-mono text-[11px] text-slate-500"
445
+ >
446
+ {{ label }}
447
+ </li>
448
+ </ul>
449
+ </UFormField>
450
+
451
+ <UFormField
452
+ v-else
405
453
  :label="t('settings.sharedStacks.add.composeFiles')"
406
454
  :help="t('settings.sharedStacks.add.composeFilesHelp')"
407
455
  >
@@ -95,7 +95,13 @@ export function createSaveActions(ctx: WizardContext) {
95
95
  await board.updateBlock(id, {
96
96
  provisioning: {
97
97
  type: 'docker-compose',
98
- ...(pruned.composeFiles?.[0] ? { composePath: pruned.composeFiles[0] } : {}),
98
+ // `composePath` is the single-file fallback the provider uses when a recipe declares no
99
+ // layers, so only a bare in-repo path can fill it. The wizard's layers always are ones
100
+ // (they come from the deterministic detector); an `inline` / other-repo layer, which the
101
+ // API can supply, simply leaves it unset — the recipe below already carries the layer.
102
+ ...(typeof pruned.composeFiles?.[0] === 'string'
103
+ ? { composePath: pruned.composeFiles[0] }
104
+ : {}),
99
105
  ...(build ? { composeBuild: true } : {}),
100
106
  recipe: pruned,
101
107
  },
@@ -630,6 +630,7 @@
630
630
  "directoryHelp": "Wird nur von der automatischen Erkennung verwendet: das Monorepo-Unterverzeichnis, in dem der Compose-Stack liegt.",
631
631
  "composeFiles": "Compose-Dateien",
632
632
  "composeFilesHelp": "Kommagetrennt, repo-relativ, in Override-Reihenfolge.",
633
+ "composeLayersManagedHelp": "Über die API verwaltet — dieser Stack hat Layer, die inline bereitgestellt oder aus einem anderen Repository gelesen werden; sie werden hier schreibgeschützt angezeigt und beim Speichern nicht verändert.",
633
634
  "composeProfiles": "Compose-Profile (optional)",
634
635
  "managedNetworks": "Verwaltete Netzwerke (optional)",
635
636
  "managedNetworksHelp": "Netzwerke, die dieser Stack für Konsumenten erstellt und besitzt, um sich damit zu verbinden.",
@@ -2923,6 +2923,7 @@
2923
2923
  "directoryHelp": "Used only by Autodetect: the monorepo subdirectory the compose stack lives in.",
2924
2924
  "composeFiles": "Compose files",
2925
2925
  "composeFilesHelp": "Comma-separated, repo-relative, in override order.",
2926
+ "composeLayersManagedHelp": "Managed through the API — this stack has layers supplied inline or read from another repo, so they are shown read-only here and left untouched when you save.",
2926
2927
  "composeProfiles": "Compose profiles (optional)",
2927
2928
  "managedNetworks": "Managed networks (optional)",
2928
2929
  "managedNetworksHelp": "Networks this stack creates and owns for consumers to attach to.",
@@ -2703,6 +2703,7 @@
2703
2703
  "directoryHelp": "Solo lo usa la detección automática: el subdirectorio del monorepo donde se encuentra el stack de compose.",
2704
2704
  "composeFiles": "Archivos de Compose",
2705
2705
  "composeFilesHelp": "Separados por comas, relativos al repositorio, en orden de anulación.",
2706
+ "composeLayersManagedHelp": "Gestionado mediante la API: esta pila tiene capas suministradas en línea o leídas desde otro repositorio, por lo que aquí se muestran como solo lectura y no se modifican al guardar.",
2706
2707
  "composeProfiles": "Perfiles de Compose (opcional)",
2707
2708
  "managedNetworks": "Redes gestionadas (opcional)",
2708
2709
  "managedNetworksHelp": "Redes que este stack crea y posee para que los consumidores se conecten.",
@@ -2703,6 +2703,7 @@
2703
2703
  "directoryHelp": "Utilisé uniquement par la détection automatique : le sous-répertoire du monorepo où se trouve le stack compose.",
2704
2704
  "composeFiles": "Fichiers Compose",
2705
2705
  "composeFilesHelp": "Séparés par des virgules, relatifs au dépôt, dans l'ordre de surcharge.",
2706
+ "composeLayersManagedHelp": "Géré via l'API — cette pile comporte des couches fournies en ligne ou lues depuis un autre dépôt ; elles sont affichées en lecture seule ici et restent intactes à l'enregistrement.",
2706
2707
  "composeProfiles": "Profils Compose (facultatif)",
2707
2708
  "managedNetworks": "Réseaux gérés (facultatif)",
2708
2709
  "managedNetworksHelp": "Réseaux que ce stack crée et possède pour que les consommateurs s'y connectent.",
@@ -2843,6 +2843,7 @@
2843
2843
  "directoryHelp": "בשימוש רק על ידי הזיהוי האוטומטי: תת-הספרייה במונורפו שבה נמצא מקבץ ה-compose.",
2844
2844
  "composeFiles": "קובצי Compose",
2845
2845
  "composeFilesHelp": "מופרדים בפסיקים, יחסית למאגר, לפי סדר הדריסה.",
2846
+ "composeLayersManagedHelp": "מנוהל דרך ה-API — למחסנית הזו יש שכבות שסופקו בתוך ההגדרה או נקראות ממאגר אחר, ולכן הן מוצגות כאן לקריאה בלבד ונשארות ללא שינוי בשמירה.",
2846
2847
  "composeProfiles": "פרופילי Compose (אופציונלי)",
2847
2848
  "managedNetworks": "רשתות מנוהלות (אופציונלי)",
2848
2849
  "managedNetworksHelp": "רשתות שהמקבץ יוצר ומחזיק כדי שצרכנים יתחברו אליהן.",
@@ -630,6 +630,7 @@
630
630
  "directoryHelp": "Usato solo dal rilevamento automatico: la sottodirectory del monorepo in cui si trova lo stack compose.",
631
631
  "composeFiles": "File compose",
632
632
  "composeFilesHelp": "Separati da virgola, relativi al repository, in ordine di override.",
633
+ "composeLayersManagedHelp": "Gestito tramite API: questo stack ha livelli forniti inline o letti da un altro repository, quindi qui sono mostrati in sola lettura e restano invariati al salvataggio.",
633
634
  "composeProfiles": "Profili compose (facoltativo)",
634
635
  "managedNetworks": "Reti gestite (facoltativo)",
635
636
  "managedNetworksHelp": "Reti che questo stack crea e possiede affinche i consumatori vi si colleghino.",
@@ -2844,6 +2844,7 @@
2844
2844
  "directoryHelp": "自動検出でのみ使用されます。compose スタックが存在するモノレポのサブディレクトリです。",
2845
2845
  "composeFiles": "Compose ファイル",
2846
2846
  "composeFilesHelp": "カンマ区切り、リポジトリ相対、オーバーライド順。",
2847
+ "composeLayersManagedHelp": "API で管理されています。このスタックにはインラインで指定された層、または別のリポジトリから読み込まれる層があるため、ここでは読み取り専用で表示され、保存時にも変更されません。",
2847
2848
  "composeProfiles": "Compose プロファイル(任意)",
2848
2849
  "managedNetworks": "マネージドネットワーク(任意)",
2849
2850
  "managedNetworksHelp": "コンシューマーが接続するために、このスタックが作成・所有するネットワーク。",
@@ -2703,6 +2703,7 @@
2703
2703
  "directoryHelp": "Używane tylko przez automatyczne wykrywanie: podkatalog monorepo, w którym znajduje się stos compose.",
2704
2704
  "composeFiles": "Pliki Compose",
2705
2705
  "composeFilesHelp": "Rozdzielone przecinkami, względem repozytorium, w kolejności nadpisywania.",
2706
+ "composeLayersManagedHelp": "Zarządzane przez API — ten stos ma warstwy podane bezpośrednio lub odczytywane z innego repozytorium, więc są tu tylko do odczytu i pozostają nietknięte przy zapisie.",
2706
2707
  "composeProfiles": "Profile Compose (opcjonalnie)",
2707
2708
  "managedNetworks": "Zarządzane sieci (opcjonalnie)",
2708
2709
  "managedNetworksHelp": "Sieci, które ten stos tworzy i posiada, aby konsumenci mogli się z nimi łączyć.",
@@ -2844,6 +2844,7 @@
2844
2844
  "directoryHelp": "Yalnızca otomatik algılama tarafından kullanılır: compose yığınının bulunduğu monorepo alt dizini.",
2845
2845
  "composeFiles": "Compose dosyaları",
2846
2846
  "composeFilesHelp": "Virgülle ayrılmış, depoya göreli, geçersiz kılma sırasında.",
2847
+ "composeLayersManagedHelp": "API üzerinden yönetilir — bu yığında satır içi verilen veya başka bir depodan okunan katmanlar var; burada salt okunur gösterilir ve kaydettiğinizde değiştirilmez.",
2847
2848
  "composeProfiles": "Compose profilleri (isteğe bağlı)",
2848
2849
  "managedNetworks": "Yönetilen ağlar (isteğe bağlı)",
2849
2850
  "managedNetworksHelp": "Tüketicilerin bağlanması için bu yığının oluşturup sahip olduğu ağlar.",
@@ -2703,6 +2703,7 @@
2703
2703
  "directoryHelp": "Використовується лише автовизначенням: підкаталог монорепозиторію, де розташований стек compose.",
2704
2704
  "composeFiles": "Файли Compose",
2705
2705
  "composeFilesHelp": "Через кому, відносно репозиторію, у порядку перевизначення.",
2706
+ "composeLayersManagedHelp": "Керується через API — цей стек має шари, задані безпосередньо або зчитані з іншого репозиторію, тож тут вони показані лише для читання й не змінюються під час збереження.",
2706
2707
  "composeProfiles": "Профілі Compose (необов'язково)",
2707
2708
  "managedNetworks": "Керовані мережі (необов'язково)",
2708
2709
  "managedNetworksHelp": "Мережі, які цей стек створює й якими володіє, щоб споживачі під'єднувалися до них.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.189.0",
3
+ "version": "0.190.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",
@@ -40,7 +40,7 @@
40
40
  "valibot": "^1.4.2",
41
41
  "vue": "3.5.40",
42
42
  "wretch": "^3.0.9",
43
- "@cat-factory/contracts": "0.196.0"
43
+ "@cat-factory/contracts": "0.197.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@toad-contracts/testing": "0.3.2",