@cat-factory/app 0.153.2 → 0.153.4
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/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/package.json +2 -2
|
@@ -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
|
+
}
|
package/app/stores/github.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import type {
|
|
4
|
-
CreateBranchInput,
|
|
5
4
|
GitHubAvailableRepo,
|
|
6
5
|
GitHubBranch,
|
|
7
6
|
GitHubConnection,
|
|
@@ -9,18 +8,15 @@ import type {
|
|
|
9
8
|
GitHubIssue,
|
|
10
9
|
GitHubPullRequest,
|
|
11
10
|
GitHubRepo,
|
|
12
|
-
MergePullRequestInput,
|
|
13
|
-
OpenPullRequestInput,
|
|
14
11
|
RepoTreeEntry,
|
|
15
|
-
ResyncRequest,
|
|
16
12
|
} from '~/types/domain'
|
|
17
13
|
import { useSingleFlightProbe } from '~/composables/useSingleFlightProbe'
|
|
18
14
|
import { useUpsertList } from '~/composables/useUpsertList'
|
|
19
15
|
import { useWorkspaceStore } from '~/stores/workspace'
|
|
20
16
|
import { useServicesStore } from '~/stores/services'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
import { pullKey, type GitHubStoreContext } from '~/stores/github/context'
|
|
18
|
+
import { createGitHubConnectionActions } from '~/stores/github/connection'
|
|
19
|
+
import { createGitHubRepoActions } from '~/stores/github/repoActions'
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
22
|
* GitHub integration state: the workspace's App installation, the projected
|
|
@@ -155,170 +151,35 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
155
151
|
if (connected.value && repos.value.length === 0) await load()
|
|
156
152
|
}
|
|
157
153
|
|
|
158
|
-
/**
|
|
159
|
-
* Load the repos the installation can access, with this workspace's link state.
|
|
160
|
-
* With a `q` the backend filters `owner/name` server-side (the add-service picker
|
|
161
|
-
* searches instead of prefetching a huge installation); without one it browses all
|
|
162
|
-
* (the repo-link panel). A blank/short `q` clears the list rather than fetching.
|
|
163
|
-
*/
|
|
164
|
-
async function loadAvailableRepos(q?: string) {
|
|
165
|
-
if (!connected.value) return
|
|
166
|
-
if (q !== undefined && q.trim() === '') {
|
|
167
|
-
availableRepos.value = []
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
loadingAvailable.value = true
|
|
171
|
-
try {
|
|
172
|
-
availableRepos.value = await api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
173
|
-
} finally {
|
|
174
|
-
loadingAvailable.value = false
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Search the installation/PAT-accessible repos server-side WITHOUT touching the shared
|
|
180
|
-
* `availableRepos`/`loadingAvailable` singleton — it returns the matches to the caller instead.
|
|
181
|
-
* This is the reusable form behind {@link useRepoSearch}: two independent pickers (the
|
|
182
|
-
* add-service modal and the doc-task reference-repo picker) can search concurrently without
|
|
183
|
-
* clobbering each other's results. A blank/short `q` (or no connection) returns `[]`.
|
|
184
|
-
*/
|
|
185
|
-
async function searchAvailableRepos(q: string): Promise<GitHubAvailableRepo[]> {
|
|
186
|
-
if (!connected.value || q.trim() === '') return []
|
|
187
|
-
return api.listGitHubAvailableRepos(workspace.requireId(), q)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Set the exact set of repos this workspace links, then refresh projections. */
|
|
191
|
-
async function setLinkedRepos(repoGithubIds: number[]) {
|
|
192
|
-
savingRepos.value = true
|
|
193
|
-
try {
|
|
194
|
-
repos.value = await api.setGitHubLinkedRepos(workspace.requireId(), repoGithubIds)
|
|
195
|
-
// Reflect the new link state in the picker and refresh PRs/issues.
|
|
196
|
-
const linked = new Set(repoGithubIds)
|
|
197
|
-
availableRepos.value = availableRepos.value.map((r) => ({
|
|
198
|
-
...r,
|
|
199
|
-
linked: linked.has(r.githubId),
|
|
200
|
-
}))
|
|
201
|
-
await load()
|
|
202
|
-
} finally {
|
|
203
|
-
savingRepos.value = false
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Lazily load (and cache) the branches for a single repo. */
|
|
208
|
-
async function loadBranches(repoGithubId: number): Promise<GitHubBranch[]> {
|
|
209
|
-
const list = await api.listGitHubBranches(workspace.requireId(), repoGithubId)
|
|
210
|
-
branches.value = { ...branches.value, [repoGithubId]: list }
|
|
211
|
-
return list
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** List one level of a (monorepo) repo's tree, for the service-directory picker. */
|
|
215
|
-
function loadRepoTree(repoGithubId: number, path = '') {
|
|
216
|
-
return api.listGitHubRepoTree(workspace.requireId(), repoGithubId, path)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
154
|
/** Full file listing per repo (recursive tree), cached by GitHub numeric id. */
|
|
220
155
|
const repoFiles = ref<Record<number, RepoTreeEntry[]>>({})
|
|
221
156
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
async function loadInstallations() {
|
|
246
|
-
loadingInstallations.value = true
|
|
247
|
-
try {
|
|
248
|
-
const { installations: list } = await api.listGitHubInstallations(workspace.requireId())
|
|
249
|
-
installations.value = list
|
|
250
|
-
} finally {
|
|
251
|
-
loadingInstallations.value = false
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Programmatic bind by installation id (the browser flow uses the redirect). */
|
|
256
|
-
async function connect(installationId: number) {
|
|
257
|
-
connection.value = await api.connectGitHub(workspace.requireId(), installationId)
|
|
258
|
-
available.value = true
|
|
259
|
-
await load()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function disconnect() {
|
|
263
|
-
await api.disconnectGitHub(workspace.requireId())
|
|
264
|
-
connection.value = null
|
|
265
|
-
repos.value = []
|
|
266
|
-
availableRepos.value = []
|
|
267
|
-
pulls.value = []
|
|
268
|
-
issues.value = []
|
|
269
|
-
branches.value = {}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Trigger a resync, then refresh projections (no-op for queued/backfill). */
|
|
273
|
-
async function resync(body: ResyncRequest = {}) {
|
|
274
|
-
syncing.value = true
|
|
275
|
-
try {
|
|
276
|
-
const res = await api.resyncGitHub(workspace.requireId(), body)
|
|
277
|
-
await load()
|
|
278
|
-
return res
|
|
279
|
-
} finally {
|
|
280
|
-
syncing.value = false
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ---- repo writes ----------------------------------------------------------
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Create a repository under the connected account (privileged App tier). Only
|
|
288
|
-
* meaningful when `canCreateRepos`; the backend 409s otherwise. Returns the
|
|
289
|
-
* created repo so the caller can confirm/link it.
|
|
290
|
-
*/
|
|
291
|
-
function createRepo(input: Parameters<typeof api.createGitHubRepo>[1]) {
|
|
292
|
-
return api.createGitHubRepo(workspace.requireId(), input)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function createBranch(repoGithubId: number, input: CreateBranchInput) {
|
|
296
|
-
const branch = await api.createGitHubBranch(workspace.requireId(), repoGithubId, input)
|
|
297
|
-
const next = branches.value[repoGithubId] ?? []
|
|
298
|
-
branches.value = { ...branches.value, [repoGithubId]: [branch, ...next] }
|
|
299
|
-
return branch
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function openPullRequest(repoGithubId: number, input: OpenPullRequestInput) {
|
|
303
|
-
const pr = await api.openGitHubPullRequest(workspace.requireId(), repoGithubId, input)
|
|
304
|
-
upsertPull(pr)
|
|
305
|
-
return pr
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function mergePullRequest(
|
|
309
|
-
repoGithubId: number,
|
|
310
|
-
number: number,
|
|
311
|
-
input: MergePullRequestInput = {},
|
|
312
|
-
) {
|
|
313
|
-
await api.mergeGitHubPullRequest(workspace.requireId(), repoGithubId, number, input)
|
|
314
|
-
// Optimistically reflect the merge until the next sync confirms it.
|
|
315
|
-
const existing = getPull(pullKey(repoGithubId, number))
|
|
316
|
-
if (existing) upsertPull({ ...existing, state: 'closed', merged: true })
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function comment(repoGithubId: number, number: number, body: string) {
|
|
320
|
-
return api.commentGitHubIssue(workspace.requireId(), repoGithubId, number, body)
|
|
157
|
+
// The installation lifecycle + the per-repo reads/writes, split into cohesive factories
|
|
158
|
+
// sharing the state above (a size-only extraction mirroring `stores/board/` — behaviour is
|
|
159
|
+
// identical to the former in-closure functions).
|
|
160
|
+
const context: GitHubStoreContext = {
|
|
161
|
+
api,
|
|
162
|
+
workspace,
|
|
163
|
+
available,
|
|
164
|
+
connection,
|
|
165
|
+
installations,
|
|
166
|
+
loadingInstallations,
|
|
167
|
+
repos,
|
|
168
|
+
availableRepos,
|
|
169
|
+
loadingAvailable,
|
|
170
|
+
savingRepos,
|
|
171
|
+
pulls,
|
|
172
|
+
upsertPull,
|
|
173
|
+
getPull,
|
|
174
|
+
issues,
|
|
175
|
+
branches,
|
|
176
|
+
repoFiles,
|
|
177
|
+
syncing,
|
|
178
|
+
connected,
|
|
179
|
+
load,
|
|
321
180
|
}
|
|
181
|
+
const connectionActions = createGitHubConnectionActions(context)
|
|
182
|
+
const repoActions = createGitHubRepoActions(context)
|
|
322
183
|
|
|
323
184
|
/**
|
|
324
185
|
* Drop the per-workspace projection + connection state (called on workspace switch)
|
|
@@ -365,23 +226,9 @@ export const useGitHubStore = defineStore('github', () => {
|
|
|
365
226
|
ensureProbed,
|
|
366
227
|
load,
|
|
367
228
|
ensureLoaded,
|
|
368
|
-
loadAvailableRepos,
|
|
369
|
-
searchAvailableRepos,
|
|
370
|
-
setLinkedRepos,
|
|
371
|
-
loadRepoTree,
|
|
372
229
|
repoFiles,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
getInstallUrl,
|
|
376
|
-
loadInstallations,
|
|
377
|
-
connect,
|
|
378
|
-
disconnect,
|
|
379
|
-
resync,
|
|
380
|
-
createRepo,
|
|
381
|
-
createBranch,
|
|
382
|
-
openPullRequest,
|
|
383
|
-
mergePullRequest,
|
|
384
|
-
comment,
|
|
230
|
+
...connectionActions,
|
|
231
|
+
...repoActions,
|
|
385
232
|
reset,
|
|
386
233
|
}
|
|
387
234
|
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
InitiativeExecutionPolicy,
|
|
4
|
+
PromoteInitiativeFollowUpInput,
|
|
5
|
+
UpdateInitiativeItemInput,
|
|
6
|
+
} from '~/types/domain'
|
|
7
|
+
import type { InitiativeActionContext } from './planning'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The tracker-curation actions: promoting / dismissing a harvested follow-up, editing one
|
|
11
|
+
* tracker item (or driving its status), and replacing the execution policy. Every one runs
|
|
12
|
+
* through the shared `curate` wrapper so the window has a single in-flight flag.
|
|
13
|
+
*/
|
|
14
|
+
export function createInitiativeCurationActions(ctx: InitiativeActionContext) {
|
|
15
|
+
const { api, workspace, upsert } = ctx
|
|
16
|
+
|
|
17
|
+
/** True while a curation action (promote/dismiss/edit item/edit policy) is in flight. */
|
|
18
|
+
const curating = ref(false)
|
|
19
|
+
|
|
20
|
+
async function curate<T>(fn: () => Promise<T>): Promise<T> {
|
|
21
|
+
if (!workspace.workspaceId) throw new Error('No active workspace')
|
|
22
|
+
curating.value = true
|
|
23
|
+
try {
|
|
24
|
+
return await fn()
|
|
25
|
+
} finally {
|
|
26
|
+
curating.value = false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Promote an `open` harvested follow-up into a new pending tracker item. */
|
|
31
|
+
async function promoteFollowUp(
|
|
32
|
+
initiativeId: string,
|
|
33
|
+
followUpId: string,
|
|
34
|
+
input: PromoteInitiativeFollowUpInput,
|
|
35
|
+
) {
|
|
36
|
+
return curate(async () => {
|
|
37
|
+
const updated = await api.promoteInitiativeFollowUp(
|
|
38
|
+
workspace.workspaceId!,
|
|
39
|
+
initiativeId,
|
|
40
|
+
followUpId,
|
|
41
|
+
input,
|
|
42
|
+
)
|
|
43
|
+
upsert(updated)
|
|
44
|
+
return updated
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Dismiss a harvested follow-up. */
|
|
49
|
+
async function dismissFollowUp(initiativeId: string, followUpId: string) {
|
|
50
|
+
return curate(async () => {
|
|
51
|
+
const updated = await api.dismissInitiativeFollowUp(
|
|
52
|
+
workspace.workspaceId!,
|
|
53
|
+
initiativeId,
|
|
54
|
+
followUpId,
|
|
55
|
+
)
|
|
56
|
+
upsert(updated)
|
|
57
|
+
return updated
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Edit one tracker item and/or drive its status (retry a blocked item / skip it). */
|
|
62
|
+
async function updateItem(
|
|
63
|
+
initiativeId: string,
|
|
64
|
+
itemId: string,
|
|
65
|
+
input: UpdateInitiativeItemInput,
|
|
66
|
+
) {
|
|
67
|
+
return curate(async () => {
|
|
68
|
+
const updated = await api.updateInitiativeItem(
|
|
69
|
+
workspace.workspaceId!,
|
|
70
|
+
initiativeId,
|
|
71
|
+
itemId,
|
|
72
|
+
input,
|
|
73
|
+
)
|
|
74
|
+
upsert(updated)
|
|
75
|
+
return updated
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Replace the execution policy (concurrency + pipeline rules). */
|
|
80
|
+
async function updatePolicy(initiativeId: string, policy: InitiativeExecutionPolicy) {
|
|
81
|
+
return curate(async () => {
|
|
82
|
+
const updated = await api.updateInitiativePolicy(workspace.workspaceId!, initiativeId, policy)
|
|
83
|
+
upsert(updated)
|
|
84
|
+
return updated
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { curating, promoteFollowUp, dismissFollowUp, updateItem, updatePolicy }
|
|
89
|
+
}
|