@cat-factory/app 1.0.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +18 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AgentFailureCard.vue +97 -0
  10. package/app/components/board/AgentStopButton.vue +61 -0
  11. package/app/components/board/BoardCanvas.vue +146 -0
  12. package/app/components/board/TaskDependencyEdges.vue +132 -0
  13. package/app/components/board/nodes/AgentChip.vue +59 -0
  14. package/app/components/board/nodes/BlockNode.vue +347 -0
  15. package/app/components/board/nodes/DecisionBadge.vue +21 -0
  16. package/app/components/board/nodes/DraggableTask.vue +69 -0
  17. package/app/components/board/nodes/ModuleFrame.vue +70 -0
  18. package/app/components/board/nodes/TaskCard.vue +237 -0
  19. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  20. package/app/components/documents/DocumentImportModal.vue +161 -0
  21. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  22. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  23. package/app/components/documents/TaskContextDocs.vue +83 -0
  24. package/app/components/focus/BlockFocusView.vue +161 -0
  25. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  26. package/app/components/github/GitHubConnect.vue +183 -0
  27. package/app/components/github/GitHubPanel.vue +584 -0
  28. package/app/components/layout/BoardSwitcher.vue +202 -0
  29. package/app/components/layout/BoardToolbar.vue +109 -0
  30. package/app/components/layout/SideBar.vue +193 -0
  31. package/app/components/layout/SpendWarningBanner.vue +107 -0
  32. package/app/components/palettes/AgentPalette.vue +33 -0
  33. package/app/components/palettes/BlockPalette.vue +41 -0
  34. package/app/components/palettes/PipelinePalette.vue +74 -0
  35. package/app/components/panels/DecisionModal.vue +71 -0
  36. package/app/components/panels/InspectorPanel.vue +296 -0
  37. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  38. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  39. package/app/components/panels/inspector/TaskExecution.vue +175 -0
  40. package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
  41. package/app/components/panels/inspector/TaskStructure.vue +139 -0
  42. package/app/components/pipeline/PipelineBuilder.vue +227 -0
  43. package/app/components/pipeline/PipelineProgress.vue +246 -0
  44. package/app/components/requirements/RequirementReviewModal.vue +328 -0
  45. package/app/components/scenarios/FeatureScenarios.vue +162 -0
  46. package/app/components/scenarios/ScenarioCard.vue +109 -0
  47. package/app/components/tasks/TaskContextIssues.vue +88 -0
  48. package/app/components/tasks/TaskImportModal.vue +140 -0
  49. package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
  50. package/app/composables/useApi.ts +535 -0
  51. package/app/composables/useBlockDrag.ts +75 -0
  52. package/app/composables/useBlockQueries.ts +136 -0
  53. package/app/composables/useBoardFlow.ts +11 -0
  54. package/app/composables/useDepLabels.ts +26 -0
  55. package/app/composables/useSemanticZoom.ts +16 -0
  56. package/app/composables/useWorkspaceStream.ts +125 -0
  57. package/app/docs/architecture.md +31 -0
  58. package/app/pages/index.vue +80 -0
  59. package/app/stores/accounts.ts +64 -0
  60. package/app/stores/agentRuns.ts +117 -0
  61. package/app/stores/agents.ts +40 -0
  62. package/app/stores/auth.ts +97 -0
  63. package/app/stores/board.spec.ts +197 -0
  64. package/app/stores/board.ts +147 -0
  65. package/app/stores/bootstrap.ts +97 -0
  66. package/app/stores/documents.ts +165 -0
  67. package/app/stores/execution.ts +115 -0
  68. package/app/stores/fragmentLibrary.ts +147 -0
  69. package/app/stores/fragments.ts +40 -0
  70. package/app/stores/github.ts +291 -0
  71. package/app/stores/models.ts +48 -0
  72. package/app/stores/pipelines.ts +77 -0
  73. package/app/stores/requirements.ts +133 -0
  74. package/app/stores/scenarios.spec.ts +82 -0
  75. package/app/stores/scenarios.ts +196 -0
  76. package/app/stores/tasks.spec.ts +71 -0
  77. package/app/stores/tasks.ts +149 -0
  78. package/app/stores/ui.ts +204 -0
  79. package/app/stores/workspace.ts +201 -0
  80. package/app/types/accounts.ts +38 -0
  81. package/app/types/bootstrap.ts +83 -0
  82. package/app/types/documents.ts +92 -0
  83. package/app/types/domain.ts +216 -0
  84. package/app/types/execution.ts +110 -0
  85. package/app/types/fragments.ts +72 -0
  86. package/app/types/github.ts +153 -0
  87. package/app/types/models.ts +48 -0
  88. package/app/types/requirements.ts +38 -0
  89. package/app/types/scenarios.ts +36 -0
  90. package/app/types/tasks.ts +67 -0
  91. package/app/utils/catalog.spec.ts +82 -0
  92. package/app/utils/catalog.ts +185 -0
  93. package/app/utils/dnd.ts +29 -0
  94. package/nuxt.config.ts +43 -0
  95. package/package.json +43 -0
@@ -0,0 +1,665 @@
1
+ <script setup lang="ts">
2
+ // Repo bootstrap: launch a "bootstrap repo" run and manage the reference
3
+ // architecture list. A run creates a new repository and has a bootstrapper agent
4
+ // adapt it (in a sandbox container) — either by cloning a chosen reference
5
+ // architecture, or from scratch following a freeform prompt. The modal pairs the
6
+ // launch form with the managed base list.
7
+ import type { BootstrapStatus, ReferenceArchitecture } from '~/types/domain'
8
+ // Explicit import (see GitHubPanel): the auto-import name for github/GitHubConnect
9
+ // doesn't match the `<GitHubConnect>` tag, so bind it directly.
10
+ import GitHubConnect from '~/components/github/GitHubConnect.vue'
11
+
12
+ const ui = useUiStore()
13
+ const bootstrap = useBootstrapStore()
14
+ const agentRuns = useAgentRunsStore()
15
+ const github = useGitHubStore()
16
+ const toast = useToast()
17
+
18
+ const open = computed({
19
+ get: () => ui.bootstrapOpen,
20
+ set: (v: boolean) => {
21
+ if (!v) ui.closeBootstrap()
22
+ },
23
+ })
24
+
25
+ // Load the workspace's reference architectures + recent jobs, plus (best-effort)
26
+ // the GitHub repos the user can access so the base form can pick from them.
27
+ watch(open, (isOpen) => {
28
+ if (isOpen) {
29
+ void bootstrap.load()
30
+ void loadGitHubRepos()
31
+ }
32
+ })
33
+
34
+ async function loadGitHubRepos() {
35
+ try {
36
+ await github.probe()
37
+ if (github.connected) await github.load()
38
+ } catch {
39
+ // GitHub integration off / unreachable → the repo picker just isn't offered.
40
+ }
41
+ }
42
+
43
+ /** Existing GitHub repos (accessible to the workspace) as `owner/name` options. */
44
+ const repoOptions = computed(() =>
45
+ github.repos.map((r) => ({ label: `${r.owner}/${r.name}`, value: `${r.owner}/${r.name}` })),
46
+ )
47
+ const hasRepoOptions = computed(() => repoOptions.value.length > 0)
48
+
49
+ // ---- launch form -----------------------------------------------------------
50
+ type LaunchMode = 'reference' | 'scratch'
51
+ const mode = ref<LaunchMode>('reference')
52
+ const modeItems = [
53
+ {
54
+ label: 'From a reference architecture',
55
+ value: 'reference' as const,
56
+ description: 'Clone a managed base repo and adapt it to the new service.',
57
+ },
58
+ {
59
+ label: 'From scratch',
60
+ value: 'scratch' as const,
61
+ description: 'Scaffold a brand-new repo from a freeform prompt — no base needed.',
62
+ },
63
+ ]
64
+
65
+ const selectedArchId = ref<string | undefined>(undefined)
66
+ const repoName = ref('')
67
+ const description = ref('')
68
+ const isPrivate = ref(true)
69
+ const instructions = ref('')
70
+ const launching = ref(false)
71
+
72
+ const usingReference = computed(() => mode.value === 'reference')
73
+
74
+ // Mirror of the backend `slugField` rule (@cat-factory/contracts bootstrap
75
+ // schema): the new repo name is a SINGLE GitHub name segment — no "owner/"
76
+ // prefix — so reject a bad value inline before we hit the API. Kept in sync with
77
+ // the contract regex by hand (the FE can't import the backend contracts package).
78
+ const REPO_NAME_RE = /^[A-Za-z0-9_.-]+$/
79
+ const repoNameError = computed<string | undefined>(() => {
80
+ const value = repoName.value.trim()
81
+ if (!value) return undefined
82
+ if (value.includes('/')) return 'Enter just the repository name — drop the “owner/” prefix.'
83
+ if (!REPO_NAME_RE.test(value)) return 'Only letters, digits, “.”, “_” and “-” are allowed.'
84
+ if (value.length > 100) return 'Must be 100 characters or fewer.'
85
+ return undefined
86
+ })
87
+
88
+ const selectedArch = computed(() =>
89
+ bootstrap.architectures.find((a) => a.id === selectedArchId.value),
90
+ )
91
+
92
+ const archOptions = computed(() =>
93
+ bootstrap.architectures.map((a) => ({
94
+ label: `${a.name} · ${a.repoOwner}/${a.repoName}`,
95
+ value: a.id,
96
+ })),
97
+ )
98
+
99
+ // Keep a sensible default selection + mode as the list loads/changes. With no
100
+ // reference architectures available, only the from-scratch flow makes sense.
101
+ watch(
102
+ () => bootstrap.architectures,
103
+ (list) => {
104
+ if (!selectedArchId.value && list.length) selectedArchId.value = list[0]!.id
105
+ if (!list.length) mode.value = 'scratch'
106
+ },
107
+ { immediate: true },
108
+ )
109
+
110
+ // A bootstrap run pushes into a GitHub repo, so the workspace must be connected
111
+ // first (the backend pre-flights the same and 409s otherwise). When the
112
+ // integration is on but unconnected, surface the discover-and-link prompt inline
113
+ // and block launch until it's bound.
114
+ const needsGitHub = computed(() => github.available === true && !github.connected)
115
+
116
+ // The account the repo must live under — the connected installation's account. The
117
+ // run pushes into an existing repo here (cat-factory doesn't create it: a GitHub App
118
+ // can't create repos under a personal account, and we'd rather not hold the broad
119
+ // Administration permission). The repo must be empty or hold only a prepopulated
120
+ // README/.gitignore/license — the push force-overwrites that boilerplate. The
121
+ // convenience link opens GitHub's new-repo page prefilled so the user can create it
122
+ // in one click.
123
+ const repoOwner = computed(() => github.connection?.accountLogin ?? '')
124
+ const createRepoUrl = computed(() => {
125
+ const params = new URLSearchParams()
126
+ if (repoOwner.value) params.set('owner', repoOwner.value)
127
+ const name = repoName.value.trim()
128
+ if (name) params.set('name', name)
129
+ const desc = description.value.trim()
130
+ if (desc) params.set('description', desc)
131
+ params.set('visibility', isPrivate.value ? 'private' : 'public')
132
+ return `https://github.com/new?${params.toString()}`
133
+ })
134
+
135
+ const creatingRepo = ref(false)
136
+
137
+ // The "create repository" button behaves differently per tier. Restricted orgs
138
+ // (the default) open GitHub's new-repo page prefilled — cat-factory needs no
139
+ // repo-creation permission. Privileged orgs (the connection reports
140
+ // `canCreateRepos`) create it programmatically via the backend, with no page.
141
+ async function openCreateRepo() {
142
+ const name = repoName.value.trim()
143
+ if (!name || repoNameError.value) return
144
+
145
+ if (!github.canCreateRepos) {
146
+ window.open(createRepoUrl.value, '_blank', 'noopener')
147
+ return
148
+ }
149
+
150
+ creatingRepo.value = true
151
+ try {
152
+ const repo = await github.createRepo({
153
+ name,
154
+ private: isPrivate.value,
155
+ description: description.value.trim() || undefined,
156
+ })
157
+ toast.add({
158
+ title: 'Repository created',
159
+ description: `${repo.owner}/${repo.name}`,
160
+ icon: 'i-lucide-check',
161
+ color: 'success',
162
+ })
163
+ } catch (e) {
164
+ toast.add({
165
+ title: 'Could not create repository',
166
+ description: e instanceof Error ? e.message : String(e),
167
+ icon: 'i-lucide-triangle-alert',
168
+ color: 'error',
169
+ })
170
+ } finally {
171
+ creatingRepo.value = false
172
+ }
173
+ }
174
+
175
+ // After the repo is created, the App still needs access to it: a "selected
176
+ // repositories" installation can't see a brand-new repo, so the run 404s with
177
+ // "not accessible to the GitHub App". Link straight to the connected
178
+ // installation's settings page, where the user adds the repo to its access list
179
+ // in one click — no install/connect round-trip (the workspace is already bound).
180
+ const manageInstallUrl = computed(() => {
181
+ const conn = github.connection
182
+ if (!conn) return undefined
183
+ return conn.targetType === 'Organization'
184
+ ? `https://github.com/organizations/${conn.accountLogin}/settings/installations/${conn.installationId}`
185
+ : `https://github.com/settings/installations/${conn.installationId}`
186
+ })
187
+
188
+ function openManageInstall() {
189
+ if (manageInstallUrl.value) window.open(manageInstallUrl.value, '_blank', 'noopener')
190
+ }
191
+
192
+ const canLaunch = computed(() => {
193
+ if (needsGitHub.value) return false
194
+ if (!repoName.value.trim() || repoNameError.value) return false
195
+ return usingReference.value ? !!selectedArchId.value : instructions.value.trim().length > 0
196
+ })
197
+
198
+ async function launch() {
199
+ if (!canLaunch.value) return
200
+ launching.value = true
201
+ try {
202
+ const job = await bootstrap.bootstrap({
203
+ referenceArchitectureId: usingReference.value ? (selectedArchId.value ?? null) : null,
204
+ repoName: repoName.value.trim(),
205
+ description: description.value.trim(),
206
+ private: isPrivate.value,
207
+ instructions: instructions.value.trim(),
208
+ })
209
+ if (job.status === 'failed') {
210
+ // The container couldn't even start (pre-flight failure, e.g. the target
211
+ // repo isn't empty) — surfaced synchronously, before any board frame.
212
+ toast.add({
213
+ title: 'Bootstrap failed',
214
+ description: job.error ?? 'The bootstrapper reported a failure.',
215
+ icon: 'i-lucide-triangle-alert',
216
+ color: 'error',
217
+ })
218
+ } else {
219
+ // Running: the container is spinning up. A provisional service card now
220
+ // shows on the board and tracks live progress; the run continues in the
221
+ // background and becomes a real, droppable service when it finishes.
222
+ toast.add({
223
+ title: 'Bootstrapping started',
224
+ description: `A container is bootstrapping ${job.repoName} — watch its progress on the board.`,
225
+ icon: 'i-lucide-loader-circle',
226
+ color: 'info',
227
+ })
228
+ repoName.value = ''
229
+ description.value = ''
230
+ instructions.value = ''
231
+ // The run is now tracked on the board, so get out of the way: close the
232
+ // dialog as soon as bootstrapping has actually started.
233
+ ui.closeBootstrap()
234
+ }
235
+ } catch (e) {
236
+ toast.add({
237
+ title: 'Could not bootstrap',
238
+ description: e instanceof Error ? e.message : String(e),
239
+ icon: 'i-lucide-triangle-alert',
240
+ color: 'error',
241
+ })
242
+ } finally {
243
+ launching.value = false
244
+ }
245
+ }
246
+
247
+ // ---- reference architecture management -------------------------------------
248
+ type ArchForm = {
249
+ id: string | null
250
+ name: string
251
+ repoOwner: string
252
+ repoName: string
253
+ description: string
254
+ defaultInstructions: string
255
+ }
256
+ const blankForm = (): ArchForm => ({
257
+ id: null,
258
+ name: '',
259
+ repoOwner: '',
260
+ repoName: '',
261
+ description: '',
262
+ defaultInstructions: '',
263
+ })
264
+ const archForm = ref<ArchForm>(blankForm())
265
+ const showArchForm = ref(false)
266
+ const savingArch = ref(false)
267
+ /** The `owner/name` slug picked from the GitHub repo list, when used. */
268
+ const archRepoSlug = ref<string | undefined>(undefined)
269
+
270
+ /** Match the form's current owner/name against an available repo option. */
271
+ function slugForForm(): string | undefined {
272
+ if (!archForm.value.repoOwner || !archForm.value.repoName) return undefined
273
+ const slug = `${archForm.value.repoOwner}/${archForm.value.repoName}`
274
+ return repoOptions.value.some((o) => o.value === slug) ? slug : undefined
275
+ }
276
+
277
+ // Picking an existing repo fills owner/name (and seeds the name when still blank).
278
+ watch(archRepoSlug, (slug) => {
279
+ if (!slug) return
280
+ const sep = slug.indexOf('/')
281
+ if (sep < 0) return
282
+ archForm.value.repoOwner = slug.slice(0, sep)
283
+ archForm.value.repoName = slug.slice(sep + 1)
284
+ if (!archForm.value.name.trim()) archForm.value.name = archForm.value.repoName
285
+ })
286
+
287
+ function startCreate() {
288
+ archForm.value = blankForm()
289
+ archRepoSlug.value = undefined
290
+ showArchForm.value = true
291
+ }
292
+ function startEdit(a: ReferenceArchitecture) {
293
+ archForm.value = {
294
+ id: a.id,
295
+ name: a.name,
296
+ repoOwner: a.repoOwner,
297
+ repoName: a.repoName,
298
+ description: a.description,
299
+ defaultInstructions: a.defaultInstructions,
300
+ }
301
+ archRepoSlug.value = slugForForm()
302
+ showArchForm.value = true
303
+ }
304
+
305
+ const canSaveArch = computed(
306
+ () =>
307
+ archForm.value.name.trim() && archForm.value.repoOwner.trim() && archForm.value.repoName.trim(),
308
+ )
309
+
310
+ async function saveArch() {
311
+ if (!canSaveArch.value) return
312
+ savingArch.value = true
313
+ try {
314
+ const body = {
315
+ name: archForm.value.name.trim(),
316
+ repoOwner: archForm.value.repoOwner.trim(),
317
+ repoName: archForm.value.repoName.trim(),
318
+ description: archForm.value.description.trim(),
319
+ defaultInstructions: archForm.value.defaultInstructions.trim(),
320
+ }
321
+ if (archForm.value.id) await bootstrap.updateArchitecture(archForm.value.id, body)
322
+ else await bootstrap.createArchitecture(body)
323
+ showArchForm.value = false
324
+ archForm.value = blankForm()
325
+ archRepoSlug.value = undefined
326
+ } catch (e) {
327
+ toast.add({
328
+ title: 'Could not save reference architecture',
329
+ description: e instanceof Error ? e.message : String(e),
330
+ icon: 'i-lucide-triangle-alert',
331
+ color: 'error',
332
+ })
333
+ } finally {
334
+ savingArch.value = false
335
+ }
336
+ }
337
+
338
+ async function removeArch(a: ReferenceArchitecture) {
339
+ try {
340
+ await bootstrap.deleteArchitecture(a.id)
341
+ if (selectedArchId.value === a.id) selectedArchId.value = undefined
342
+ } catch (e) {
343
+ toast.add({
344
+ title: 'Could not delete',
345
+ description: e instanceof Error ? e.message : String(e),
346
+ icon: 'i-lucide-triangle-alert',
347
+ color: 'error',
348
+ })
349
+ }
350
+ }
351
+
352
+ const statusColor: Record<BootstrapStatus, 'neutral' | 'info' | 'success' | 'error'> = {
353
+ pending: 'neutral',
354
+ running: 'info',
355
+ succeeded: 'success',
356
+ failed: 'error',
357
+ }
358
+ </script>
359
+
360
+ <template>
361
+ <UModal v-model:open="open" title="Bootstrap a repository" :ui="{ content: 'max-w-2xl' }">
362
+ <template #body>
363
+ <div class="space-y-6">
364
+ <p class="text-sm text-slate-400">
365
+ Create an empty GitHub repository, then let a bootstrapper agent populate it in a sandbox
366
+ container — either by adapting one of your reference architectures, or from scratch
367
+ following a freeform prompt. cat-factory pushes the initial commit into that repo;
368
+ {{
369
+ github.canCreateRepos
370
+ ? 'for this account it can create the repository for you too.'
371
+ : 'you create the repository (one click below), so it needs no repo-creation permission.'
372
+ }}
373
+ </p>
374
+
375
+ <!-- not connected: a run needs GitHub, so discover & link before launching -->
376
+ <div
377
+ v-if="needsGitHub"
378
+ class="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
379
+ >
380
+ <div class="flex items-start gap-2">
381
+ <UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
382
+ <p class="text-sm text-amber-200/90">
383
+ Connect this workspace to GitHub before bootstrapping — a run pushes into a
384
+ repository. Link an installation the App is already on, or install it.
385
+ </p>
386
+ </div>
387
+ <GitHubConnect />
388
+ </div>
389
+
390
+ <!-- launch -->
391
+ <section class="space-y-4">
392
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
393
+ New repository
394
+ </h3>
395
+
396
+ <UFormField label="How should we start?" required>
397
+ <URadioGroup v-model="mode" :items="modeItems" />
398
+ </UFormField>
399
+
400
+ <template v-if="usingReference">
401
+ <UFormField
402
+ label="Reference architecture"
403
+ description="The managed base repo to clone and adapt."
404
+ required
405
+ >
406
+ <div v-if="!bootstrap.hasArchitectures" class="text-sm text-slate-400">
407
+ No reference architectures yet — add one below, or switch to “From scratch”.
408
+ </div>
409
+ <USelect
410
+ v-else
411
+ v-model="selectedArchId"
412
+ :items="archOptions"
413
+ placeholder="Choose a reference architecture"
414
+ class="w-full"
415
+ />
416
+ </UFormField>
417
+ </template>
418
+
419
+ <UFormField
420
+ label="Target repository name"
421
+ :description="
422
+ repoOwner
423
+ ? `Create a fresh repo with this name under ${repoOwner}, then bootstrap pushes into it. A prepopulated README, .gitignore or license is fine.`
424
+ : 'Create a fresh repo with this name, then bootstrap pushes into it. A prepopulated README, .gitignore or license is fine.'
425
+ "
426
+ required
427
+ :error="repoNameError"
428
+ >
429
+ <div class="space-y-2">
430
+ <div class="flex items-center gap-2">
431
+ <UInput v-model="repoName" placeholder="payments-service" class="w-full" />
432
+ <UButton
433
+ color="neutral"
434
+ variant="subtle"
435
+ :icon="github.canCreateRepos ? 'i-lucide-plus' : 'i-lucide-external-link'"
436
+ :loading="creatingRepo"
437
+ :disabled="!repoName.trim() || !!repoNameError"
438
+ :title="
439
+ github.canCreateRepos
440
+ ? 'Create the repository now'
441
+ : `Open GitHub's new-repository page, prefilled`
442
+ "
443
+ @click="openCreateRepo"
444
+ >
445
+ {{ github.canCreateRepos ? 'Create repository' : 'Create on GitHub' }}
446
+ </UButton>
447
+ </div>
448
+ <UButton
449
+ v-if="manageInstallUrl && !github.canCreateRepos"
450
+ color="neutral"
451
+ variant="ghost"
452
+ size="sm"
453
+ icon="i-lucide-shield-check"
454
+ trailing-icon="i-lucide-external-link"
455
+ title="Open the App's installation settings to grant it access to the new repo"
456
+ @click="openManageInstall"
457
+ >
458
+ Grant the App access to this repo
459
+ </UButton>
460
+ </div>
461
+ </UFormField>
462
+
463
+ <UFormField label="Description" description="Optional one-line summary for the repo.">
464
+ <UInput
465
+ v-model="description"
466
+ placeholder="Handles payment intents and refunds"
467
+ class="w-full"
468
+ />
469
+ </UFormField>
470
+
471
+ <UFormField
472
+ :label="
473
+ usingReference
474
+ ? 'Extra instructions for the bootstrapper'
475
+ : 'What should the bootstrapper build?'
476
+ "
477
+ :description="
478
+ usingReference
479
+ ? 'Optional — appended to the reference architecture’s default instructions.'
480
+ : 'Describe the new service: stack, structure, and what it should do.'
481
+ "
482
+ :required="!usingReference"
483
+ >
484
+ <UTextarea
485
+ v-model="instructions"
486
+ :rows="usingReference ? 3 : 5"
487
+ :placeholder="
488
+ usingReference
489
+ ? 'e.g. rename the package to payments, drop the example queue worker'
490
+ : 'e.g. a TypeScript Hono API with a /health route, Vitest tests, and a Dockerfile'
491
+ "
492
+ class="w-full"
493
+ />
494
+ </UFormField>
495
+
496
+ <UFormField label="Visibility">
497
+ <div class="flex items-center gap-2">
498
+ <USwitch v-model="isPrivate" />
499
+ <span class="text-sm text-slate-300">Private repository</span>
500
+ </div>
501
+ </UFormField>
502
+
503
+ <div class="flex justify-end">
504
+ <UButton
505
+ color="primary"
506
+ icon="i-lucide-rocket"
507
+ :loading="launching"
508
+ :disabled="!canLaunch"
509
+ @click="launch"
510
+ >
511
+ Bootstrap repo
512
+ </UButton>
513
+ </div>
514
+ </section>
515
+
516
+ <!-- recent jobs -->
517
+ <section v-if="agentRuns.bootstrapJobs.length" class="space-y-2">
518
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
519
+ Recent runs
520
+ </h3>
521
+ <div
522
+ v-for="job in agentRuns.bootstrapJobs.slice(0, 5)"
523
+ :key="job.id"
524
+ class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2 text-sm"
525
+ >
526
+ <div class="min-w-0">
527
+ <div class="truncate text-slate-200">{{ job.repoName }}</div>
528
+ <div class="truncate text-[11px] text-slate-500">
529
+ {{
530
+ job.referenceArchitectureName
531
+ ? `from ${job.referenceArchitectureName}`
532
+ : 'from scratch'
533
+ }}
534
+ </div>
535
+ </div>
536
+ <div class="flex items-center gap-2">
537
+ <ULink
538
+ v-if="job.repoUrl"
539
+ :to="job.repoUrl"
540
+ target="_blank"
541
+ class="text-[11px] text-indigo-400 hover:underline"
542
+ >
543
+ Open
544
+ </ULink>
545
+ <UBadge :color="statusColor[job.status]" variant="subtle" size="sm">
546
+ {{ job.status }}
547
+ </UBadge>
548
+ </div>
549
+ </div>
550
+ </section>
551
+
552
+ <USeparator />
553
+
554
+ <!-- reference architecture management -->
555
+ <section class="space-y-3">
556
+ <div class="flex items-center justify-between">
557
+ <h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
558
+ Reference architectures
559
+ </h3>
560
+ <UButton
561
+ size="xs"
562
+ color="neutral"
563
+ variant="soft"
564
+ icon="i-lucide-plus"
565
+ @click="startCreate"
566
+ >
567
+ Add
568
+ </UButton>
569
+ </div>
570
+
571
+ <div
572
+ v-for="a in bootstrap.architectures"
573
+ :key="a.id"
574
+ class="flex items-center justify-between gap-2 rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2"
575
+ >
576
+ <div class="min-w-0">
577
+ <div class="truncate text-sm text-slate-200">{{ a.name }}</div>
578
+ <div class="truncate text-[11px] text-slate-500">
579
+ {{ a.repoOwner }}/{{ a.repoName }}
580
+ </div>
581
+ </div>
582
+ <div class="flex items-center gap-1">
583
+ <UButton
584
+ size="xs"
585
+ color="neutral"
586
+ variant="ghost"
587
+ icon="i-lucide-pencil"
588
+ @click="startEdit(a)"
589
+ />
590
+ <UButton
591
+ size="xs"
592
+ color="error"
593
+ variant="ghost"
594
+ icon="i-lucide-trash-2"
595
+ @click="removeArch(a)"
596
+ />
597
+ </div>
598
+ </div>
599
+
600
+ <!-- add / edit form -->
601
+ <div
602
+ v-if="showArchForm"
603
+ class="space-y-3 rounded-md border border-slate-700 bg-slate-900/80 p-3"
604
+ >
605
+ <UFormField
606
+ v-if="hasRepoOptions"
607
+ label="Pick an existing GitHub repo"
608
+ description="Choose a repo you can access to fill in its owner and name, or enter them manually below."
609
+ >
610
+ <USelect
611
+ v-model="archRepoSlug"
612
+ :items="repoOptions"
613
+ placeholder="owner/name"
614
+ class="w-full"
615
+ />
616
+ </UFormField>
617
+
618
+ <UFormField label="Name" description="A friendly label for this base." required>
619
+ <UInput v-model="archForm.name" placeholder="Service Template" class="w-full" />
620
+ </UFormField>
621
+ <div class="grid grid-cols-2 gap-2">
622
+ <UFormField label="Repo owner" required>
623
+ <UInput v-model="archForm.repoOwner" placeholder="acme" class="w-full" />
624
+ </UFormField>
625
+ <UFormField label="Repo name" required>
626
+ <UInput v-model="archForm.repoName" placeholder="service-template" class="w-full" />
627
+ </UFormField>
628
+ </div>
629
+ <UFormField label="Description">
630
+ <UInput
631
+ v-model="archForm.description"
632
+ placeholder="Optional summary of this base"
633
+ class="w-full"
634
+ />
635
+ </UFormField>
636
+ <UFormField
637
+ label="Default bootstrapper instructions"
638
+ description="Prepended to the per-run instructions whenever this base is used."
639
+ >
640
+ <UTextarea
641
+ v-model="archForm.defaultInstructions"
642
+ :rows="2"
643
+ placeholder="e.g. keep the structure; rename packages to match the new service"
644
+ class="w-full"
645
+ />
646
+ </UFormField>
647
+ <div class="flex justify-end gap-2">
648
+ <UButton color="neutral" variant="ghost" @click="showArchForm = false">
649
+ Cancel
650
+ </UButton>
651
+ <UButton
652
+ color="primary"
653
+ :loading="savingArch"
654
+ :disabled="!canSaveArch"
655
+ @click="saveArch"
656
+ >
657
+ {{ archForm.id ? 'Save' : 'Add' }}
658
+ </UButton>
659
+ </div>
660
+ </div>
661
+ </section>
662
+ </div>
663
+ </template>
664
+ </UModal>
665
+ </template>