@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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +18 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +146 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +347 -0
- package/app/components/board/nodes/DecisionBadge.vue +21 -0
- package/app/components/board/nodes/DraggableTask.vue +69 -0
- package/app/components/board/nodes/ModuleFrame.vue +70 -0
- package/app/components/board/nodes/TaskCard.vue +237 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +161 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/layout/BoardSwitcher.vue +202 -0
- package/app/components/layout/BoardToolbar.vue +109 -0
- package/app/components/layout/SideBar.vue +193 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/palettes/AgentPalette.vue +33 -0
- package/app/components/palettes/BlockPalette.vue +41 -0
- package/app/components/palettes/PipelinePalette.vue +74 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +296 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskExecution.vue +175 -0
- package/app/components/panels/inspector/TaskModelSettings.vue +128 -0
- package/app/components/panels/inspector/TaskStructure.vue +139 -0
- package/app/components/pipeline/PipelineBuilder.vue +227 -0
- package/app/components/pipeline/PipelineProgress.vue +246 -0
- package/app/components/requirements/RequirementReviewModal.vue +328 -0
- package/app/components/scenarios/FeatureScenarios.vue +162 -0
- package/app/components/scenarios/ScenarioCard.vue +109 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +140 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +122 -0
- package/app/composables/useApi.ts +535 -0
- package/app/composables/useBlockDrag.ts +75 -0
- package/app/composables/useBlockQueries.ts +136 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useSemanticZoom.ts +16 -0
- package/app/composables/useWorkspaceStream.ts +125 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +80 -0
- package/app/stores/accounts.ts +64 -0
- package/app/stores/agentRuns.ts +117 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/auth.ts +97 -0
- package/app/stores/board.spec.ts +197 -0
- package/app/stores/board.ts +147 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/documents.ts +165 -0
- package/app/stores/execution.ts +115 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +291 -0
- package/app/stores/models.ts +48 -0
- package/app/stores/pipelines.ts +77 -0
- package/app/stores/requirements.ts +133 -0
- package/app/stores/scenarios.spec.ts +82 -0
- package/app/stores/scenarios.ts +196 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +149 -0
- package/app/stores/ui.ts +204 -0
- package/app/stores/workspace.ts +201 -0
- package/app/types/accounts.ts +38 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/documents.ts +92 -0
- package/app/types/domain.ts +216 -0
- package/app/types/execution.ts +110 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +153 -0
- package/app/types/models.ts +48 -0
- package/app/types/requirements.ts +38 -0
- package/app/types/scenarios.ts +36 -0
- package/app/types/tasks.ts +67 -0
- package/app/utils/catalog.spec.ts +82 -0
- package/app/utils/catalog.ts +185 -0
- package/app/utils/dnd.ts +29 -0
- package/nuxt.config.ts +43 -0
- 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>
|