@cat-factory/app 0.53.0 → 0.54.1

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.
@@ -14,6 +14,7 @@ const bootstrap = useBootstrapStore()
14
14
  const agentRuns = useAgentRunsStore()
15
15
  const github = useGitHubStore()
16
16
  const toast = useToast()
17
+ const { t } = useI18n()
17
18
 
18
19
  const open = computed({
19
20
  get: () => ui.bootstrapOpen,
@@ -53,18 +54,18 @@ const hasRepoOptions = computed(() => repoOptions.value.length > 0)
53
54
  // ---- launch form -----------------------------------------------------------
54
55
  type LaunchMode = 'reference' | 'scratch'
55
56
  const mode = ref<LaunchMode>('reference')
56
- const modeItems = [
57
+ const modeItems = computed(() => [
57
58
  {
58
- label: 'From a reference architecture',
59
+ label: t('bootstrap.mode.reference.label'),
59
60
  value: 'reference' as const,
60
- description: 'Clone a managed base repo and adapt it to the new service.',
61
+ description: t('bootstrap.mode.reference.description'),
61
62
  },
62
63
  {
63
- label: 'From scratch',
64
+ label: t('bootstrap.mode.scratch.label'),
64
65
  value: 'scratch' as const,
65
- description: 'Scaffold a brand-new repo from a freeform prompt — no base needed.',
66
+ description: t('bootstrap.mode.scratch.description'),
66
67
  },
67
- ]
68
+ ])
68
69
 
69
70
  const selectedArchId = ref<string | undefined>(undefined)
70
71
  const repoName = ref('')
@@ -83,9 +84,9 @@ const REPO_NAME_RE = /^[A-Za-z0-9_.-]+$/
83
84
  const repoNameError = computed<string | undefined>(() => {
84
85
  const value = repoName.value.trim()
85
86
  if (!value) return undefined
86
- if (value.includes('/')) return 'Enter just the repository name — drop the “owner/” prefix.'
87
- if (!REPO_NAME_RE.test(value)) return 'Only letters, digits, “.”, “_” and “-” are allowed.'
88
- if (value.length > 100) return 'Must be 100 characters or fewer.'
87
+ if (value.includes('/')) return t('bootstrap.repoName.error.hasSlash')
88
+ if (!REPO_NAME_RE.test(value)) return t('bootstrap.repoName.error.invalidChars')
89
+ if (value.length > 100) return t('bootstrap.repoName.error.tooLong')
89
90
  return undefined
90
91
  })
91
92
 
@@ -159,14 +160,14 @@ async function openCreateRepo() {
159
160
  description: description.value.trim() || undefined,
160
161
  })
161
162
  toast.add({
162
- title: 'Repository created',
163
+ title: t('bootstrap.toast.repoCreated'),
163
164
  description: `${repo.owner}/${repo.name}`,
164
165
  icon: 'i-lucide-check',
165
166
  color: 'success',
166
167
  })
167
168
  } catch (e) {
168
169
  toast.add({
169
- title: 'Could not create repository',
170
+ title: t('bootstrap.toast.repoCreateFailed'),
170
171
  description: e instanceof Error ? e.message : String(e),
171
172
  icon: 'i-lucide-triangle-alert',
172
173
  color: 'error',
@@ -214,8 +215,8 @@ async function launch() {
214
215
  // The container couldn't even start (pre-flight failure, e.g. the target
215
216
  // repo isn't empty) — surfaced synchronously, before any board frame.
216
217
  toast.add({
217
- title: 'Bootstrap failed',
218
- description: job.error ?? 'The bootstrapper reported a failure.',
218
+ title: t('bootstrap.toast.failed'),
219
+ description: job.error ?? t('bootstrap.toast.failedFallback'),
219
220
  icon: 'i-lucide-triangle-alert',
220
221
  color: 'error',
221
222
  })
@@ -224,8 +225,8 @@ async function launch() {
224
225
  // shows on the board and tracks live progress; the run continues in the
225
226
  // background and becomes a real, droppable service when it finishes.
226
227
  toast.add({
227
- title: 'Bootstrapping started',
228
- description: `A container is bootstrapping ${job.repoName} — watch its progress on the board.`,
228
+ title: t('bootstrap.toast.started'),
229
+ description: t('bootstrap.toast.startedDesc', { repo: job.repoName }),
229
230
  icon: 'i-lucide-loader-circle',
230
231
  color: 'info',
231
232
  })
@@ -238,7 +239,7 @@ async function launch() {
238
239
  }
239
240
  } catch (e) {
240
241
  toast.add({
241
- title: 'Could not bootstrap',
242
+ title: t('bootstrap.toast.bootstrapFailed'),
242
243
  description: e instanceof Error ? e.message : String(e),
243
244
  icon: 'i-lucide-triangle-alert',
244
245
  color: 'error',
@@ -329,7 +330,7 @@ async function saveArch() {
329
330
  archRepoSlug.value = undefined
330
331
  } catch (e) {
331
332
  toast.add({
332
- title: 'Could not save reference architecture',
333
+ title: t('bootstrap.toast.saveArchFailed'),
333
334
  description: e instanceof Error ? e.message : String(e),
334
335
  icon: 'i-lucide-triangle-alert',
335
336
  color: 'error',
@@ -345,7 +346,7 @@ async function removeArch(a: ReferenceArchitecture) {
345
346
  if (selectedArchId.value === a.id) selectedArchId.value = undefined
346
347
  } catch (e) {
347
348
  toast.add({
348
- title: 'Could not delete',
349
+ title: t('bootstrap.toast.deleteFailed'),
349
350
  description: e instanceof Error ? e.message : String(e),
350
351
  icon: 'i-lucide-triangle-alert',
351
352
  color: 'error',
@@ -359,21 +360,22 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
359
360
  succeeded: 'success',
360
361
  failed: 'error',
361
362
  }
363
+
364
+ // Exhaustive status→label map of literal `t(...)` keys (keeps the typed-key drift guard live).
365
+ const statusLabel = computed<Record<BootstrapStatus, string>>(() => ({
366
+ pending: t('bootstrap.status.pending'),
367
+ running: t('bootstrap.status.running'),
368
+ succeeded: t('bootstrap.status.succeeded'),
369
+ failed: t('bootstrap.status.failed'),
370
+ }))
362
371
  </script>
363
372
 
364
373
  <template>
365
- <UModal v-model:open="open" title="Bootstrap a repository" :ui="{ content: 'max-w-2xl' }">
374
+ <UModal v-model:open="open" :title="t('bootstrap.title')" :ui="{ content: 'max-w-2xl' }">
366
375
  <template #body>
367
376
  <div class="space-y-6">
368
377
  <p class="text-sm text-slate-400">
369
- Create an empty GitHub repository, then let a bootstrapper agent populate it in a sandbox
370
- container — either by adapting one of your reference architectures, or from scratch
371
- following a freeform prompt. cat-factory pushes the initial commit into that repo;
372
- {{
373
- github.canCreateRepos
374
- ? 'for this account it can create the repository for you too.'
375
- : 'you create the repository (one click below), so it needs no repo-creation permission.'
376
- }}
378
+ {{ github.canCreateRepos ? t('bootstrap.intro.canCreate') : t('bootstrap.intro.manual') }}
377
379
  </p>
378
380
 
379
381
  <!-- not connected: a run needs GitHub, so discover & link before launching -->
@@ -384,8 +386,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
384
386
  <div class="flex items-start gap-2">
385
387
  <UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
386
388
  <p class="text-sm text-amber-200/90">
387
- Connect this workspace to GitHub before bootstrapping — a run pushes into a
388
- repository. Link an installation the App is already on, or install it.
389
+ {{ t('bootstrap.github.prompt') }}
389
390
  </p>
390
391
  </div>
391
392
  <GitHubConnect />
@@ -394,38 +395,38 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
394
395
  <!-- launch -->
395
396
  <section class="space-y-4">
396
397
  <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
397
- New repository
398
+ {{ t('bootstrap.section.newRepo') }}
398
399
  </h3>
399
400
 
400
- <UFormField label="How should we start?" required>
401
+ <UFormField :label="t('bootstrap.mode.label')" required>
401
402
  <URadioGroup v-model="mode" :items="modeItems" />
402
403
  </UFormField>
403
404
 
404
405
  <template v-if="usingReference">
405
406
  <UFormField
406
- label="Reference architecture"
407
- description="The managed base repo to clone and adapt."
407
+ :label="t('bootstrap.reference.label')"
408
+ :description="t('bootstrap.reference.description')"
408
409
  required
409
410
  >
410
411
  <div v-if="!bootstrap.hasArchitectures" class="text-sm text-slate-400">
411
- No reference architectures yet — add one below, or switch to “From scratch”.
412
+ {{ t('bootstrap.reference.empty') }}
412
413
  </div>
413
414
  <USelect
414
415
  v-else
415
416
  v-model="selectedArchId"
416
417
  :items="archOptions"
417
- placeholder="Choose a reference architecture"
418
+ :placeholder="t('bootstrap.reference.placeholder')"
418
419
  class="w-full"
419
420
  />
420
421
  </UFormField>
421
422
  </template>
422
423
 
423
424
  <UFormField
424
- label="Target repository name"
425
+ :label="t('bootstrap.targetRepo.label')"
425
426
  :description="
426
427
  repoOwner
427
- ? `Create a fresh repo with this name under ${repoOwner}, then bootstrap pushes into it. A prepopulated README, .gitignore or license is fine.`
428
- : 'Create a fresh repo with this name, then bootstrap pushes into it. A prepopulated README, .gitignore or license is fine.'
428
+ ? t('bootstrap.targetRepo.descWithOwner', { owner: repoOwner })
429
+ : t('bootstrap.targetRepo.descNoOwner')
429
430
  "
430
431
  required
431
432
  :error="repoNameError"
@@ -441,12 +442,16 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
441
442
  :disabled="!repoName.trim() || !!repoNameError"
442
443
  :title="
443
444
  github.canCreateRepos
444
- ? 'Create the repository now'
445
- : `Open GitHub's new-repository page, prefilled`
445
+ ? t('bootstrap.createRepo.titleNow')
446
+ : t('bootstrap.createRepo.titleGitHub')
446
447
  "
447
448
  @click="openCreateRepo"
448
449
  >
449
- {{ github.canCreateRepos ? 'Create repository' : 'Create on GitHub' }}
450
+ {{
451
+ github.canCreateRepos
452
+ ? t('bootstrap.createRepo.now')
453
+ : t('bootstrap.createRepo.onGitHub')
454
+ }}
450
455
  </UButton>
451
456
  </div>
452
457
  <UButton
@@ -456,15 +461,18 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
456
461
  size="sm"
457
462
  icon="i-lucide-shield-check"
458
463
  trailing-icon="i-lucide-external-link"
459
- title="Open the App's installation settings to grant it access to the new repo"
464
+ :title="t('bootstrap.grantAccess.title')"
460
465
  @click="openManageInstall"
461
466
  >
462
- Grant the App access to this repo
467
+ {{ t('bootstrap.grantAccess.label') }}
463
468
  </UButton>
464
469
  </div>
465
470
  </UFormField>
466
471
 
467
- <UFormField label="Description" description="Optional one-line summary for the repo.">
472
+ <UFormField
473
+ :label="t('bootstrap.description.label')"
474
+ :description="t('bootstrap.description.help')"
475
+ >
468
476
  <UInput
469
477
  v-model="description"
470
478
  placeholder="Handles payment intents and refunds"
@@ -475,13 +483,13 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
475
483
  <UFormField
476
484
  :label="
477
485
  usingReference
478
- ? 'Extra instructions for the bootstrapper'
479
- : 'What should the bootstrapper build?'
486
+ ? t('bootstrap.instructions.labelReference')
487
+ : t('bootstrap.instructions.labelScratch')
480
488
  "
481
489
  :description="
482
490
  usingReference
483
- ? 'Optional — appended to the reference architecture’s default instructions.'
484
- : 'Describe the new service: stack, structure, and what it should do.'
491
+ ? t('bootstrap.instructions.descReference')
492
+ : t('bootstrap.instructions.descScratch')
485
493
  "
486
494
  :required="!usingReference"
487
495
  >
@@ -497,10 +505,10 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
497
505
  />
498
506
  </UFormField>
499
507
 
500
- <UFormField label="Visibility">
508
+ <UFormField :label="t('bootstrap.visibility.label')">
501
509
  <div class="flex items-center gap-2">
502
510
  <USwitch v-model="isPrivate" />
503
- <span class="text-sm text-slate-300">Private repository</span>
511
+ <span class="text-sm text-slate-300">{{ t('bootstrap.visibility.private') }}</span>
504
512
  </div>
505
513
  </UFormField>
506
514
 
@@ -512,7 +520,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
512
520
  :disabled="!canLaunch"
513
521
  @click="launch"
514
522
  >
515
- Bootstrap repo
523
+ {{ t('bootstrap.launch') }}
516
524
  </UButton>
517
525
  </div>
518
526
  </section>
@@ -520,7 +528,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
520
528
  <!-- recent jobs -->
521
529
  <section v-if="agentRuns.bootstrapJobs.length" class="space-y-2">
522
530
  <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
523
- Recent runs
531
+ {{ t('bootstrap.recent.title') }}
524
532
  </h3>
525
533
  <div
526
534
  v-for="job in agentRuns.bootstrapJobs.slice(0, 5)"
@@ -532,8 +540,8 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
532
540
  <div class="truncate text-[11px] text-slate-500">
533
541
  {{
534
542
  job.referenceArchitectureName
535
- ? `from ${job.referenceArchitectureName}`
536
- : 'from scratch'
543
+ ? t('bootstrap.recent.fromArch', { name: job.referenceArchitectureName })
544
+ : t('bootstrap.recent.fromScratch')
537
545
  }}
538
546
  </div>
539
547
  </div>
@@ -544,10 +552,10 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
544
552
  target="_blank"
545
553
  class="text-[11px] text-indigo-400 hover:underline"
546
554
  >
547
- Open
555
+ {{ t('bootstrap.recent.open') }}
548
556
  </ULink>
549
557
  <UBadge :color="statusColor[job.status]" variant="subtle" size="sm">
550
- {{ job.status }}
558
+ {{ statusLabel[job.status] }}
551
559
  </UBadge>
552
560
  </div>
553
561
  </div>
@@ -559,7 +567,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
559
567
  <section class="space-y-3">
560
568
  <div class="flex items-center justify-between">
561
569
  <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
562
- Reference architectures
570
+ {{ t('bootstrap.arch.title') }}
563
571
  </h3>
564
572
  <UButton
565
573
  size="xs"
@@ -568,7 +576,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
568
576
  icon="i-lucide-plus"
569
577
  @click="startCreate"
570
578
  >
571
- Add
579
+ {{ t('bootstrap.arch.add') }}
572
580
  </UButton>
573
581
  </div>
574
582
 
@@ -608,8 +616,8 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
608
616
  >
609
617
  <UFormField
610
618
  v-if="hasRepoOptions"
611
- label="Pick an existing GitHub repo"
612
- description="Choose a repo you can access to fill in its owner and name, or enter them manually below."
619
+ :label="t('bootstrap.arch.pickRepo.label')"
620
+ :description="t('bootstrap.arch.pickRepo.description')"
613
621
  >
614
622
  <USelect
615
623
  v-model="archRepoSlug"
@@ -619,27 +627,31 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
619
627
  />
620
628
  </UFormField>
621
629
 
622
- <UFormField label="Name" description="A friendly label for this base." required>
630
+ <UFormField
631
+ :label="t('bootstrap.arch.name.label')"
632
+ :description="t('bootstrap.arch.name.description')"
633
+ required
634
+ >
623
635
  <UInput v-model="archForm.name" placeholder="Service Template" class="w-full" />
624
636
  </UFormField>
625
637
  <div class="grid grid-cols-2 gap-2">
626
- <UFormField label="Repo owner" required>
638
+ <UFormField :label="t('bootstrap.arch.repoOwner')" required>
627
639
  <UInput v-model="archForm.repoOwner" placeholder="acme" class="w-full" />
628
640
  </UFormField>
629
- <UFormField label="Repo name" required>
641
+ <UFormField :label="t('bootstrap.arch.repoName')" required>
630
642
  <UInput v-model="archForm.repoName" placeholder="service-template" class="w-full" />
631
643
  </UFormField>
632
644
  </div>
633
- <UFormField label="Description">
645
+ <UFormField :label="t('bootstrap.description.label')">
634
646
  <UInput
635
647
  v-model="archForm.description"
636
- placeholder="Optional summary of this base"
648
+ :placeholder="t('bootstrap.arch.descriptionPlaceholder')"
637
649
  class="w-full"
638
650
  />
639
651
  </UFormField>
640
652
  <UFormField
641
- label="Default bootstrapper instructions"
642
- description="Prepended to the per-run instructions whenever this base is used."
653
+ :label="t('bootstrap.arch.defaultInstructions.label')"
654
+ :description="t('bootstrap.arch.defaultInstructions.description')"
643
655
  >
644
656
  <UTextarea
645
657
  v-model="archForm.defaultInstructions"
@@ -650,7 +662,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
650
662
  </UFormField>
651
663
  <div class="flex justify-end gap-2">
652
664
  <UButton color="neutral" variant="ghost" @click="showArchForm = false">
653
- Cancel
665
+ {{ t('common.cancel') }}
654
666
  </UButton>
655
667
  <UButton
656
668
  color="primary"
@@ -658,7 +670,7 @@ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'err
658
670
  :disabled="!canSaveArch"
659
671
  @click="saveArch"
660
672
  >
661
- {{ archForm.id ? 'Save' : 'Add' }}
673
+ {{ archForm.id ? t('common.save') : t('bootstrap.arch.add') }}
662
674
  </UButton>
663
675
  </div>
664
676
  </div>
@@ -7,27 +7,50 @@ import type { RunEnvironment, HumanTestEnvironmentStatus } from '~/types/executi
7
7
 
8
8
  defineProps<{ environment: RunEnvironment | null; degradedReason?: string | null }>()
9
9
 
10
- const ENV_STATUS_META: Record<
11
- HumanTestEnvironmentStatus,
12
- { label: string; color: string; icon: string }
13
- > = {
14
- provisioning: { label: 'Spinning up…', color: 'text-amber-300', icon: 'i-lucide-loader-circle' },
15
- ready: { label: 'Running', color: 'text-emerald-300', icon: 'i-lucide-circle-dot' },
16
- failed: { label: 'Errored', color: 'text-rose-300', icon: 'i-lucide-circle-alert' },
17
- expired: { label: 'Expired', color: 'text-slate-400', icon: 'i-lucide-circle-off' },
10
+ const { t, d } = useI18n()
11
+
12
+ // Exhaustive enum→label map of literal `t(...)` keys (keeps the typed-key drift guard
13
+ // live); the color/icon stay static, English-neutral styling.
14
+ const ENV_STATUS_META = computed<
15
+ Record<HumanTestEnvironmentStatus, { label: string; color: string; icon: string }>
16
+ >(() => ({
17
+ provisioning: {
18
+ label: t('environments.status.provisioning'),
19
+ color: 'text-amber-300',
20
+ icon: 'i-lucide-loader-circle',
21
+ },
22
+ ready: {
23
+ label: t('environments.status.ready'),
24
+ color: 'text-emerald-300',
25
+ icon: 'i-lucide-circle-dot',
26
+ },
27
+ failed: {
28
+ label: t('environments.status.failed'),
29
+ color: 'text-rose-300',
30
+ icon: 'i-lucide-circle-alert',
31
+ },
32
+ expired: {
33
+ label: t('environments.status.expired'),
34
+ color: 'text-slate-400',
35
+ icon: 'i-lucide-circle-off',
36
+ },
18
37
  tearing_down: {
19
- label: 'Shutting down…',
38
+ label: t('environments.status.tearing_down'),
20
39
  color: 'text-slate-400',
21
40
  icon: 'i-lucide-loader-circle',
22
41
  },
23
- torn_down: { label: 'Shut down', color: 'text-slate-400', icon: 'i-lucide-circle-off' },
24
- }
42
+ torn_down: {
43
+ label: t('environments.status.torn_down'),
44
+ color: 'text-slate-400',
45
+ icon: 'i-lucide-circle-off',
46
+ },
47
+ }))
25
48
  </script>
26
49
 
27
50
  <template>
28
51
  <section class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
29
52
  <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
30
- Ephemeral environment
53
+ {{ t('environments.title') }}
31
54
  </h3>
32
55
  <div v-if="environment" class="space-y-2">
33
56
  <div class="flex items-center gap-2 text-[13px]">
@@ -57,7 +80,7 @@ const ENV_STATUS_META: Record<
57
80
  {{ environment.url }}
58
81
  </a>
59
82
  <p v-if="environment.expiresAt" class="text-[11px] text-slate-500">
60
- Expires {{ new Date(environment.expiresAt).toLocaleString() }}
83
+ {{ t('environments.expires', { date: d(new Date(environment.expiresAt), 'long') }) }}
61
84
  </p>
62
85
  <!-- The verbatim provider error when the environment failed/expired. -->
63
86
  <pre
@@ -70,7 +93,7 @@ const ENV_STATUS_META: Record<
70
93
  >
71
94
  </div>
72
95
  <p v-else class="text-[12px] text-slate-500">
73
- {{ degradedReason ?? 'No ephemeral environment for this run.' }}
96
+ {{ degradedReason ?? t('environments.empty') }}
74
97
  </p>
75
98
  </section>
76
99
  </template>