@cat-factory/app 0.153.3 → 0.154.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/app/components/settings/WorkspaceSettingsPanel.vue +19 -0
- package/app/composables/useRunDeepLink.spec.ts +51 -0
- package/app/composables/useRunDeepLink.ts +91 -0
- package/app/pages/index.vue +4 -0
- package/app/stores/auth/mothership.ts +108 -0
- package/app/stores/auth/session.ts +118 -0
- package/app/stores/auth.ts +17 -167
- package/app/stores/board/mutations.ts +6 -191
- package/app/stores/board/placement.ts +203 -0
- package/app/stores/board.ts +5 -1
- package/app/stores/execution/commands.ts +197 -0
- package/app/stores/execution.ts +12 -171
- package/app/stores/github/connection.ts +60 -0
- package/app/stores/github/context.ts +46 -0
- package/app/stores/github/repoActions.ts +151 -0
- package/app/stores/github.ts +30 -183
- package/app/stores/initiative/curation.ts +89 -0
- package/app/stores/initiative/planning.ts +121 -0
- package/app/stores/initiative.ts +14 -175
- package/app/stores/workspace/hydrate.ts +105 -0
- package/app/stores/workspace.ts +6 -94
- package/app/stores/workspaceSettings.ts +1 -0
- package/i18n/locales/de.json +5 -0
- package/i18n/locales/en.json +5 -0
- package/i18n/locales/es.json +5 -0
- package/i18n/locales/fr.json +5 -0
- package/i18n/locales/he.json +5 -0
- package/i18n/locales/it.json +5 -0
- package/i18n/locales/ja.json +5 -0
- package/i18n/locales/pl.json +5 -0
- package/i18n/locales/tr.json +5 -0
- package/i18n/locales/uk.json +5 -0
- package/package.json +2 -2
package/app/stores/execution.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
ExecutionInstance,
|
|
6
|
-
Pipeline,
|
|
7
|
-
PipelineStep,
|
|
8
|
-
StepApproval,
|
|
9
|
-
} from '~/types/domain'
|
|
10
|
-
import type { RequestStepChangesInput } from '@cat-factory/contracts'
|
|
11
|
-
import type { IterationCapChoice } from '~/types/execution'
|
|
12
|
-
import { useWorkspaceStore } from '~/stores/workspace'
|
|
3
|
+
import type { Decision, ExecutionInstance, PipelineStep, StepApproval } from '~/types/domain'
|
|
4
|
+
import { createExecutionCommands } from '~/stores/execution/commands'
|
|
13
5
|
|
|
14
6
|
/**
|
|
15
7
|
* Running pipeline instances. The simulation engine lives on the backend: this
|
|
16
8
|
* store mirrors the server's executions and drives them via the API. Commands
|
|
17
9
|
* call the worker and then refresh the workspace snapshot, since advancing an
|
|
18
10
|
* execution also rolls status/progress up onto its block server-side.
|
|
11
|
+
*
|
|
12
|
+
* The run-control commands live in a cohesive factory ({@link createExecutionCommands}, under
|
|
13
|
+
* `stores/execution/`) that closes over the state assembled here — a size-only split mirroring
|
|
14
|
+
* `stores/board/`, not a new seam.
|
|
19
15
|
*/
|
|
20
16
|
export const useExecutionStore = defineStore('execution', () => {
|
|
21
17
|
const api = useApi()
|
|
@@ -225,101 +221,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
225
221
|
const decisionsByBlock = computed(() => groupByBlock(openDecisions.value))
|
|
226
222
|
const approvalsByBlock = computed(() => groupByBlock(openApprovals.value))
|
|
227
223
|
|
|
228
|
-
/**
|
|
229
|
-
* Start `pipeline` against a block; the server marks the block in-progress. A block
|
|
230
|
-
* pinned to an individual-usage model (Claude) needs the initiator's personal
|
|
231
|
-
* password — supplied transparently from the local cache, and prompted via the
|
|
232
|
-
* credential modal (then retried) when the server replies 428.
|
|
233
|
-
*/
|
|
234
|
-
async function start(blockId: string, pipeline: Pipeline): Promise<boolean> {
|
|
235
|
-
const ws = useWorkspaceStore()
|
|
236
|
-
const personal = usePersonalSubscriptionsStore()
|
|
237
|
-
// Returns false when the user cancels the personal-password prompt OR the start was
|
|
238
|
-
// refused (a 409 conflict, surfaced as an actionable toast here), so an optimistic
|
|
239
|
-
// caller can revert its "Starting…" state without its own error handling.
|
|
240
|
-
try {
|
|
241
|
-
return await personal.withCredential(async (password) => {
|
|
242
|
-
await api.startExecution(ws.requireId(), blockId, { pipelineId: pipeline.id }, password)
|
|
243
|
-
await ws.refresh()
|
|
244
|
-
})
|
|
245
|
-
} catch (e) {
|
|
246
|
-
runErrors.present(e, 'errors.action.startFailed')
|
|
247
|
-
return false
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Interacting with a running individual-usage run (resolve/approve/request-changes) advances
|
|
252
|
-
// + re-dispatches the run, so the server re-mints its short-TTL activation from the personal
|
|
253
|
-
// password first. It rides the cached password transparently, and — like start/retry — is
|
|
254
|
-
// gated through `withCredential`: a within-buffer/lapsed cache re-prompts EARLY here (while
|
|
255
|
-
// the user is present) rather than letting the run break mid-pipeline. For a non-individual
|
|
256
|
-
// run the server ignores it and nothing prompts.
|
|
257
|
-
async function resolveDecision(instanceId: string, decisionId: string, choice: string) {
|
|
258
|
-
const ws = useWorkspaceStore()
|
|
259
|
-
const personal = usePersonalSubscriptionsStore()
|
|
260
|
-
return await personal.withCredential(async (password) => {
|
|
261
|
-
await api.resolveDecision(ws.requireId(), instanceId, decisionId, { choice }, password)
|
|
262
|
-
await ws.refresh()
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** Approve a step's gated proposal (optionally edited); the run advances. */
|
|
267
|
-
async function approveStep(instanceId: string, approvalId: string, proposal?: string) {
|
|
268
|
-
const ws = useWorkspaceStore()
|
|
269
|
-
const personal = usePersonalSubscriptionsStore()
|
|
270
|
-
return await personal.withCredential(async (password) => {
|
|
271
|
-
await api.approveStep(ws.requireId(), instanceId, approvalId, { proposal }, password)
|
|
272
|
-
await ws.refresh()
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** Request changes on a gated proposal; the step re-runs with the review. */
|
|
277
|
-
async function requestStepChanges(
|
|
278
|
-
instanceId: string,
|
|
279
|
-
approvalId: string,
|
|
280
|
-
review: RequestStepChangesInput,
|
|
281
|
-
) {
|
|
282
|
-
const ws = useWorkspaceStore()
|
|
283
|
-
const personal = usePersonalSubscriptionsStore()
|
|
284
|
-
return await personal.withCredential(async (password) => {
|
|
285
|
-
await api.requestStepChanges(ws.requireId(), instanceId, approvalId, review, password)
|
|
286
|
-
await ws.refresh()
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Reject a gated proposal; the run stops entirely (a retryable failure). */
|
|
291
|
-
async function rejectStep(instanceId: string, approvalId: string, reason?: string) {
|
|
292
|
-
const ws = useWorkspaceStore()
|
|
293
|
-
await api.rejectStep(ws.requireId(), instanceId, approvalId, { reason })
|
|
294
|
-
await ws.refresh()
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Resolve a companion step parked at its rework cap: extra-round (one more pass) /
|
|
299
|
-
* proceed (advance with the current output) / stop-reset (cancel + reset the task).
|
|
300
|
-
* Rides the cached personal password (gated through `withCredential`, so a within-buffer
|
|
301
|
-
* cache re-prompts early) for the server to re-mint the run's activation before
|
|
302
|
-
* re-dispatching on extra-round/proceed.
|
|
303
|
-
*/
|
|
304
|
-
async function resolveCompanionExceeded(
|
|
305
|
-
instanceId: string,
|
|
306
|
-
approvalId: string,
|
|
307
|
-
choice: IterationCapChoice,
|
|
308
|
-
) {
|
|
309
|
-
const ws = useWorkspaceStore()
|
|
310
|
-
const personal = usePersonalSubscriptionsStore()
|
|
311
|
-
return await personal.withCredential(async (password) => {
|
|
312
|
-
await api.resolveCompanionExceeded(
|
|
313
|
-
ws.requireId(),
|
|
314
|
-
instanceId,
|
|
315
|
-
approvalId,
|
|
316
|
-
{ choice },
|
|
317
|
-
password,
|
|
318
|
-
)
|
|
319
|
-
await ws.refresh()
|
|
320
|
-
})
|
|
321
|
-
}
|
|
322
|
-
|
|
323
224
|
/** How many approval gates anywhere are awaiting a human. */
|
|
324
225
|
const pendingApprovalCount = computed(() =>
|
|
325
226
|
instances.value.reduce(
|
|
@@ -328,62 +229,11 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
328
229
|
),
|
|
329
230
|
)
|
|
330
231
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
await ws.refresh()
|
|
337
|
-
} catch (e) {
|
|
338
|
-
runErrors.present(e, 'errors.action.mergeFailed')
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Restart a run from a chosen step: the server re-runs from `stepIndex` onward
|
|
344
|
-
* (resetting that step + later steps' iteration counters) while preserving the
|
|
345
|
-
* earlier steps' outputs as handoff context, and re-drives a fresh run. Like
|
|
346
|
-
* start/retry it may dispatch an individual-usage (Claude) step, so it rides the
|
|
347
|
-
* initiator's personal password — prompted (then retried) on a 428. Returns false
|
|
348
|
-
* when the user cancels that prompt (nothing was restarted).
|
|
349
|
-
*/
|
|
350
|
-
async function restartFromStep(instanceId: string, stepIndex: number): Promise<boolean> {
|
|
351
|
-
const ws = useWorkspaceStore()
|
|
352
|
-
const personal = usePersonalSubscriptionsStore()
|
|
353
|
-
try {
|
|
354
|
-
return await personal.withCredential(async (password) => {
|
|
355
|
-
await api.restartFromStep(ws.requireId(), instanceId, stepIndex, password)
|
|
356
|
-
await ws.refresh()
|
|
357
|
-
})
|
|
358
|
-
} catch (e) {
|
|
359
|
-
runErrors.present(e, 'errors.action.restartFailed')
|
|
360
|
-
return false
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Cancel the execution running against a block and reset it to planned. `workspaceId`
|
|
366
|
-
* defaults to the current workspace but can be pinned by callers that cancel a run for a
|
|
367
|
-
* board the user may have since navigated away from (e.g. a deferred delete's commit).
|
|
368
|
-
*/
|
|
369
|
-
async function cancel(blockId: string, workspaceId?: string) {
|
|
370
|
-
const ws = useWorkspaceStore()
|
|
371
|
-
await api.cancelExecution(workspaceId ?? ws.requireId(), blockId)
|
|
372
|
-
instances.value = instances.value.filter((e) => e.blockId !== blockId)
|
|
373
|
-
await ws.refresh()
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Stop a running execution WITHOUT deleting it: halts the container + durable driver
|
|
378
|
-
* and records the run as `cancelled` (a retryable failure), leaving the block
|
|
379
|
-
* `blocked`. Unlike {@link cancel} the run is kept — its steps/output stay readable on
|
|
380
|
-
* the board and it can be retried from where it stopped. `runId` is the execution id.
|
|
381
|
-
*/
|
|
382
|
-
async function stop(runId: string) {
|
|
383
|
-
const ws = useWorkspaceStore()
|
|
384
|
-
await api.stopAgentRun(ws.requireId(), runId)
|
|
385
|
-
await ws.refresh()
|
|
386
|
-
}
|
|
232
|
+
// The run-control commands (start / decide / approve / merge / restart / cancel / stop),
|
|
233
|
+
// extracted into a cohesive factory sharing the state above (a size-only split mirroring
|
|
234
|
+
// `stores/board/` and `stores/pipelines/` — behaviour is identical to the former in-closure
|
|
235
|
+
// functions).
|
|
236
|
+
const commands = createExecutionCommands({ api, runErrors, instances })
|
|
387
237
|
|
|
388
238
|
return {
|
|
389
239
|
instances,
|
|
@@ -398,15 +248,6 @@ export const useExecutionStore = defineStore('execution', () => {
|
|
|
398
248
|
decisionsByBlock,
|
|
399
249
|
approvalsByBlock,
|
|
400
250
|
pendingApprovalCount,
|
|
401
|
-
|
|
402
|
-
resolveDecision,
|
|
403
|
-
approveStep,
|
|
404
|
-
requestStepChanges,
|
|
405
|
-
rejectStep,
|
|
406
|
-
resolveCompanionExceeded,
|
|
407
|
-
restartFromStep,
|
|
408
|
-
mergePr,
|
|
409
|
-
cancel,
|
|
410
|
-
stop,
|
|
251
|
+
...commands,
|
|
411
252
|
}
|
|
412
253
|
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ResyncRequest } from '~/types/domain'
|
|
2
|
+
import type { GitHubStoreContext } from './context'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The App-installation lifecycle: discovering/binding an installation, dropping it, and
|
|
6
|
+
* triggering a resync of the projections. Extracted from the store setup; each operation closes
|
|
7
|
+
* over the shared {@link GitHubStoreContext} so behaviour is identical to the original
|
|
8
|
+
* in-closure functions — the split is purely to keep every function within the size budget.
|
|
9
|
+
*/
|
|
10
|
+
export function createGitHubConnectionActions(ctx: GitHubStoreContext) {
|
|
11
|
+
const { api, workspace, available, connection, installations, loadingInstallations } = ctx
|
|
12
|
+
const { repos, availableRepos, pulls, issues, branches, syncing, load } = ctx
|
|
13
|
+
|
|
14
|
+
/** The URL a workspace owner visits to install the App against this workspace. */
|
|
15
|
+
function getInstallUrl(): Promise<string> {
|
|
16
|
+
return api.getGitHubInstallUrl(workspace.requireId()).then((r) => r.url)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Discover the App's installations so the user can connect one without typing an id. */
|
|
20
|
+
async function loadInstallations() {
|
|
21
|
+
loadingInstallations.value = true
|
|
22
|
+
try {
|
|
23
|
+
const { installations: list } = await api.listGitHubInstallations(workspace.requireId())
|
|
24
|
+
installations.value = list
|
|
25
|
+
} finally {
|
|
26
|
+
loadingInstallations.value = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Programmatic bind by installation id (the browser flow uses the redirect). */
|
|
31
|
+
async function connect(installationId: number) {
|
|
32
|
+
connection.value = await api.connectGitHub(workspace.requireId(), installationId)
|
|
33
|
+
available.value = true
|
|
34
|
+
await load()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function disconnect() {
|
|
38
|
+
await api.disconnectGitHub(workspace.requireId())
|
|
39
|
+
connection.value = null
|
|
40
|
+
repos.value = []
|
|
41
|
+
availableRepos.value = []
|
|
42
|
+
pulls.value = []
|
|
43
|
+
issues.value = []
|
|
44
|
+
branches.value = {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Trigger a resync, then refresh projections (no-op for queued/backfill). */
|
|
48
|
+
async function resync(body: ResyncRequest = {}) {
|
|
49
|
+
syncing.value = true
|
|
50
|
+
try {
|
|
51
|
+
const res = await api.resyncGitHub(workspace.requireId(), body)
|
|
52
|
+
await load()
|
|
53
|
+
return res
|
|
54
|
+
} finally {
|
|
55
|
+
syncing.value = false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { getInstallUrl, loadInstallations, connect, disconnect, resync }
|
|
60
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
GitHubBranch,
|
|
4
|
+
GitHubConnection,
|
|
5
|
+
GitHubAvailableRepo,
|
|
6
|
+
GitHubInstallationOption,
|
|
7
|
+
GitHubIssue,
|
|
8
|
+
GitHubPullRequest,
|
|
9
|
+
GitHubRepo,
|
|
10
|
+
RepoTreeEntry,
|
|
11
|
+
} from '~/types/domain'
|
|
12
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
13
|
+
|
|
14
|
+
/** Stable identity for a pull request in the `pulls` list: repo + PR number. */
|
|
15
|
+
export const pullKey = (repoGithubId: number, number: number) => `${repoGithubId}:${number}`
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared reactive state + injected dependencies the GitHub-store action factories close over.
|
|
19
|
+
* Created once in the `github` store setup and threaded into {@link createGitHubConnectionActions}
|
|
20
|
+
* and {@link createGitHubRepoActions} so the split operations stay behaviourally identical to the
|
|
21
|
+
* original single-closure store — a size-only extraction mirroring `stores/board/` and
|
|
22
|
+
* `stores/pipelines/`, not a new seam.
|
|
23
|
+
*/
|
|
24
|
+
export interface GitHubStoreContext {
|
|
25
|
+
api: ReturnType<typeof useApi>
|
|
26
|
+
workspace: ReturnType<typeof useWorkspaceStore>
|
|
27
|
+
available: Ref<boolean | null>
|
|
28
|
+
connection: Ref<GitHubConnection | null>
|
|
29
|
+
installations: Ref<GitHubInstallationOption[]>
|
|
30
|
+
loadingInstallations: Ref<boolean>
|
|
31
|
+
repos: Ref<GitHubRepo[]>
|
|
32
|
+
availableRepos: Ref<GitHubAvailableRepo[]>
|
|
33
|
+
loadingAvailable: Ref<boolean>
|
|
34
|
+
savingRepos: Ref<boolean>
|
|
35
|
+
pulls: Ref<GitHubPullRequest[]>
|
|
36
|
+
upsertPull: (pull: GitHubPullRequest) => void
|
|
37
|
+
getPull: (key: string) => GitHubPullRequest | undefined
|
|
38
|
+
issues: Ref<GitHubIssue[]>
|
|
39
|
+
branches: Ref<Record<number, GitHubBranch[]>>
|
|
40
|
+
repoFiles: Ref<Record<number, RepoTreeEntry[]>>
|
|
41
|
+
syncing: Ref<boolean>
|
|
42
|
+
/** Whether an App installation is bound (the store's `connected` computed). */
|
|
43
|
+
connected: Readonly<Ref<boolean>>
|
|
44
|
+
/** Reload the cached repos/PRs/issues projection (the store's `load`). */
|
|
45
|
+
load: () => Promise<void>
|
|
46
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateBranchInput,
|
|
3
|
+
GitHubAvailableRepo,
|
|
4
|
+
GitHubBranch,
|
|
5
|
+
MergePullRequestInput,
|
|
6
|
+
OpenPullRequestInput,
|
|
7
|
+
RepoTreeEntry,
|
|
8
|
+
} from '~/types/domain'
|
|
9
|
+
import { pullKey, type GitHubStoreContext } from './context'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The per-repo reads (link picker, branches, trees, file listings) and writes (create repo /
|
|
13
|
+
* branch, open / merge a PR, comment). Extracted from the store setup; each operation closes
|
|
14
|
+
* over the shared {@link GitHubStoreContext} so behaviour is identical to the original
|
|
15
|
+
* in-closure functions — the split is purely to keep every function within the size budget.
|
|
16
|
+
*/
|
|
17
|
+
export function createGitHubRepoActions(ctx: GitHubStoreContext) {
|
|
18
|
+
const { api, workspace, connected, repos, availableRepos, loadingAvailable, savingRepos } = ctx
|
|
19
|
+
const { branches, repoFiles, upsertPull, getPull, load } = ctx
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load the repos the installation can access, with this workspace's link state.
|
|
23
|
+
* With a `q` the backend filters `owner/name` server-side (the add-service picker
|
|
24
|
+
* searches instead of prefetching a huge installation); without one it browses all
|
|
25
|
+
* (the repo-link panel). A blank/short `q` clears the list rather than fetching.
|
|
26
|
+
*/
|
|
27
|
+
async function loadAvailableRepos(q?: string) {
|
|
28
|
+
if (!connected.value) return
|
|
29
|
+
if (q !== undefined && q.trim() === '') {
|
|
30
|
+
availableRepos.value = []
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
loadingAvailable.value = true
|
|
34
|
+
try {
|
|
35
|
+
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
36
|
+
} finally {
|
|
37
|
+
loadingAvailable.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Search the installation/PAT-accessible repos server-side WITHOUT touching the shared
|
|
43
|
+
* `availableRepos`/`loadingAvailable` singleton — it returns the matches to the caller instead.
|
|
44
|
+
* This is the reusable form behind `useRepoSearch`: two independent pickers (the add-service
|
|
45
|
+
* modal and the doc-task reference-repo picker) can search concurrently without clobbering
|
|
46
|
+
* each other's results. A blank/short `q` (or no connection) returns `[]`.
|
|
47
|
+
*/
|
|
48
|
+
async function searchAvailableRepos(q: string): Promise<GitHubAvailableRepo[]> {
|
|
49
|
+
if (!connected.value || q.trim() === '') return []
|
|
50
|
+
return api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Set the exact set of repos this workspace links, then refresh projections. */
|
|
54
|
+
async function setLinkedRepos(repoGithubIds: number[]) {
|
|
55
|
+
savingRepos.value = true
|
|
56
|
+
try {
|
|
57
|
+
repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
|
|
58
|
+
// Reflect the new link state in the picker and refresh PRs/issues.
|
|
59
|
+
const linked = new Set(repoGithubIds)
|
|
60
|
+
availableRepos.value = availableRepos.value.map((r) => ({
|
|
61
|
+
...r,
|
|
62
|
+
linked: linked.has(r.githubId),
|
|
63
|
+
}))
|
|
64
|
+
await load()
|
|
65
|
+
} finally {
|
|
66
|
+
savingRepos.value = false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Lazily load (and cache) the branches for a single repo. */
|
|
71
|
+
async function loadBranches(repoGithubId: number): Promise<GitHubBranch[]> {
|
|
72
|
+
const list = await api.listGitHubBranches(workspace.requireId(), repoGithubId)
|
|
73
|
+
branches.value = { ...branches.value, [repoGithubId]: list }
|
|
74
|
+
return list
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** List one level of a (monorepo) repo's tree, for the service-directory picker. */
|
|
78
|
+
function loadRepoTree(repoGithubId: number, path = '') {
|
|
79
|
+
return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load (and cache) EVERY file in a repo — the whole tree in one recursive read — so a
|
|
84
|
+
* picker can search files by path entirely client-side (no per-keystroke server call).
|
|
85
|
+
* Cached by repo id so re-opening the picker for the same repo is instant; force a
|
|
86
|
+
* refetch with `{ reload: true }`. Mirrors the branches cache.
|
|
87
|
+
*/
|
|
88
|
+
async function loadRepoFiles(
|
|
89
|
+
repoGithubId: number,
|
|
90
|
+
opts: { reload?: boolean } = {},
|
|
91
|
+
): Promise<RepoTreeEntry[]> {
|
|
92
|
+
const cached = repoFiles.value[repoGithubId]
|
|
93
|
+
if (cached && !opts.reload) return cached
|
|
94
|
+
const files = await api.listGitHubRepoFiles(workspace.requireId(), repoGithubId)
|
|
95
|
+
repoFiles.value = { ...repoFiles.value, [repoGithubId]: files }
|
|
96
|
+
return files
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- repo writes ----------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a repository under the connected account (privileged App tier). Only
|
|
103
|
+
* meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
|
|
104
|
+
* created repo so the caller can confirm/link it.
|
|
105
|
+
*/
|
|
106
|
+
function createRepo(input: Parameters<typeof api.createGitHubRepo>[1]) {
|
|
107
|
+
return api.createGitHubRepo(workspace.requireId(), input)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function createBranch(repoGithubId: number, input: CreateBranchInput) {
|
|
111
|
+
const branch = await api.createGitHubBranch(workspace.requireId(), repoGithubId, input)
|
|
112
|
+
const next = branches.value[repoGithubId] ?? []
|
|
113
|
+
branches.value = { ...branches.value, [repoGithubId]: [branch, ...next] }
|
|
114
|
+
return branch
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
|
|
118
|
+
const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
|
|
119
|
+
upsertPull(pr)
|
|
120
|
+
return pr
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function mergePullRequest(
|
|
124
|
+
repoGithubId: number,
|
|
125
|
+
number: number,
|
|
126
|
+
input: MergePullRequestInput = {},
|
|
127
|
+
) {
|
|
128
|
+
await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
|
|
129
|
+
// Optimistically reflect the merge until the next sync confirms it.
|
|
130
|
+
const existing = getPull(pullKey(repoGithubId, number))
|
|
131
|
+
if (existing) upsertPull({ ...existing, state: 'closed', merged: true })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function comment(repoGithubId: number, number: number, body: string) {
|
|
135
|
+
return api.commentGitHubIssue(workspace.requireId(), repoGithubId, number, body)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
loadAvailableRepos,
|
|
140
|
+
searchAvailableRepos,
|
|
141
|
+
setLinkedRepos,
|
|
142
|
+
loadBranches,
|
|
143
|
+
loadRepoTree,
|
|
144
|
+
loadRepoFiles,
|
|
145
|
+
createRepo,
|
|
146
|
+
createBranch,
|
|
147
|
+
openPullRequest,
|
|
148
|
+
mergePullRequest,
|
|
149
|
+
comment,
|
|
150
|
+
}
|
|
151
|
+
}
|