mintree 0.1.11 → 0.2.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/README.md +74 -17
- package/dist/commands/dashboard.js +100 -46
- package/dist/commands/doctor.js +62 -19
- package/dist/commands/init.d.ts +14 -1
- package/dist/commands/init.js +72 -13
- package/dist/commands/worktree/clean.js +2 -19
- package/dist/commands/worktree/create.js +3 -2
- package/dist/commands/worktree/list.js +10 -22
- package/dist/commands/worktree/work.js +5 -4
- package/dist/lib/branch.d.ts +7 -4
- package/dist/lib/branch.js +15 -7
- package/dist/lib/dashboard.d.ts +5 -42
- package/dist/lib/dashboard.js +33 -189
- package/dist/lib/gh.d.ts +16 -0
- package/dist/lib/{github.js → gh.js} +9 -0
- package/dist/lib/metadata.d.ts +15 -0
- package/dist/lib/metadata.js +51 -0
- package/dist/lib/pr.d.ts +26 -0
- package/dist/lib/pr.js +49 -0
- package/dist/lib/providers/github.d.ts +33 -0
- package/dist/lib/providers/github.js +381 -0
- package/dist/lib/providers/index.d.ts +27 -0
- package/dist/lib/providers/index.js +83 -0
- package/dist/lib/providers/plane.d.ts +61 -0
- package/dist/lib/providers/plane.js +749 -0
- package/dist/lib/providers/types.d.ts +113 -0
- package/dist/lib/providers/types.js +12 -0
- package/dist/lib/session-signal.d.ts +3 -2
- package/dist/lib/session-signal.js +4 -3
- package/dist/lib/worktreeCreate.js +4 -1
- package/package.json +1 -1
- package/dist/lib/github.d.ts +0 -7
- package/dist/lib/githubProject.d.ts +0 -55
- package/dist/lib/githubProject.js +0 -277
package/dist/lib/dashboard.js
CHANGED
|
@@ -1,43 +1,22 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { execFile } from "child_process";
|
|
4
|
-
import { promisify } from "util";
|
|
5
|
-
import { tryExec } from "./exec.js";
|
|
6
3
|
import { listWorktrees, getWorktreesDir, isDirty, getAheadBehind, } from "./git.js";
|
|
7
4
|
import { readMetadata } from "./metadata.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const ISSUE_LIST_LIMIT = 50;
|
|
5
|
+
import { fetchPrForBranch } from "./pr.js";
|
|
6
|
+
import { createProvider } from "./providers/index.js";
|
|
11
7
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const json = await tryExec(`gh issue list --assignee @me --state open --json number,title,state,url,labels,body,createdAt,updatedAt --limit ${ISSUE_LIST_LIMIT} 2>/dev/null`);
|
|
19
|
-
if (!json)
|
|
20
|
-
return null;
|
|
21
|
-
try {
|
|
22
|
-
const parsed = JSON.parse(json);
|
|
23
|
-
if (!Array.isArray(parsed))
|
|
24
|
-
return null;
|
|
25
|
-
return parsed;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Builds a map from issue id (number, as string) to the matching mintree
|
|
33
|
-
* worktree. IssueId comes from the worktree dir name (`<issue>-<desc>`)
|
|
34
|
-
* rather than the branch, so detached worktrees (created via the dashboard's
|
|
35
|
-
* "current branch" mode) are included alongside the regular branch-based
|
|
36
|
-
* ones. Worktrees outside `.mintree/worktrees/` are skipped.
|
|
8
|
+
* Builds a map from issue id (the canonical string — "100" on GitHub,
|
|
9
|
+
* "BACK-100" on Plane once that lands) to the matching mintree worktree.
|
|
10
|
+
* IssueId comes from the worktree dir name (`<issue>-<desc>`) rather than
|
|
11
|
+
* the branch, so detached worktrees (created via the dashboard's "current
|
|
12
|
+
* branch" mode) are included alongside the regular branch-based ones.
|
|
13
|
+
* Worktrees outside `.mintree/worktrees/` are skipped.
|
|
37
14
|
*/
|
|
38
15
|
function buildWorktreeIndex(repoRoot) {
|
|
39
16
|
const worktreesRoot = path.resolve(getWorktreesDir(repoRoot));
|
|
40
|
-
|
|
17
|
+
// Same shape as the BRANCH_REGEX issueId capture: bare digits (GitHub) or
|
|
18
|
+
// `<PROJ>-\d+` (Plane). Matches `100-foo` and `BACK-100-foo` alike.
|
|
19
|
+
const dirNameRegex = /^((?:[A-Z][A-Z0-9_]*-)?\d+)-/;
|
|
41
20
|
const index = new Map();
|
|
42
21
|
for (const w of listWorktrees(repoRoot)) {
|
|
43
22
|
const wAbs = path.resolve(w.path);
|
|
@@ -84,153 +63,6 @@ function readSessionState(repoRoot, issueId) {
|
|
|
84
63
|
function isSessionState(v) {
|
|
85
64
|
return v === "active" || v === "idle" || v === "waiting" || v === "exited";
|
|
86
65
|
}
|
|
87
|
-
function shQuote(value) {
|
|
88
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Looks up the most recent PR for a branch (any state). Returns null when
|
|
92
|
-
* there's no PR or `gh` can't reach the API. Used to populate the detail
|
|
93
|
-
* pane's "Pull Request" section.
|
|
94
|
-
*/
|
|
95
|
-
async function fetchPrForBranch(branch) {
|
|
96
|
-
const out = await tryExec(`gh pr list --head ${shQuote(branch)} --state all --json number,state,url --limit 1 2>/dev/null`);
|
|
97
|
-
if (!out)
|
|
98
|
-
return null;
|
|
99
|
-
try {
|
|
100
|
-
const arr = JSON.parse(out);
|
|
101
|
-
if (Array.isArray(arr) && arr.length > 0 && arr[0])
|
|
102
|
-
return arr[0];
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// fall through
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
// GitHub Projects v2 single-select options carry their own colour enum.
|
|
110
|
-
// Map each to the closest Ink/chalk colour; ORANGE and PINK have no 16-colour
|
|
111
|
-
// keyword so they use hex (truecolor terminals render them, others approximate).
|
|
112
|
-
const PROJECT_STATUS_COLORS = {
|
|
113
|
-
GRAY: "gray",
|
|
114
|
-
BLUE: "blue",
|
|
115
|
-
GREEN: "green",
|
|
116
|
-
YELLOW: "yellow",
|
|
117
|
-
ORANGE: "#d18616",
|
|
118
|
-
RED: "red",
|
|
119
|
-
PINK: "#d2a8ff",
|
|
120
|
-
PURPLE: "magenta",
|
|
121
|
-
};
|
|
122
|
-
const STATUS_ORDER_UNSET = 999;
|
|
123
|
-
function parseProjectNumberFromUrl(url) {
|
|
124
|
-
const m = url.match(/\/projects\/(\d+)/);
|
|
125
|
-
return m && m[1] ? Number(m[1]) : null;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Runs a GraphQL query via `gh api graphql`. Returns null on any failure
|
|
129
|
-
* (gh not authenticated, missing scope, network) — the caller degrades
|
|
130
|
-
* gracefully to an ungrouped list.
|
|
131
|
-
*/
|
|
132
|
-
async function ghGraphql(query) {
|
|
133
|
-
try {
|
|
134
|
-
const { stdout } = await execFileAsync("gh", ["api", "graphql", "-f", `query=${query}`]);
|
|
135
|
-
return JSON.parse(stdout);
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Picks which project board an issue belongs to for grouping purposes. When
|
|
143
|
-
* `.mintree/metadata.json` pins a project URL, only that board counts;
|
|
144
|
-
* otherwise the first board the issue appears on wins.
|
|
145
|
-
*/
|
|
146
|
-
function pickProjectNode(nodes, configuredUrl) {
|
|
147
|
-
if (nodes.length === 0)
|
|
148
|
-
return null;
|
|
149
|
-
if (configuredUrl) {
|
|
150
|
-
const targetNumber = parseProjectNumberFromUrl(configuredUrl);
|
|
151
|
-
return (nodes.find((n) => n.project?.url === configuredUrl ||
|
|
152
|
-
(targetNumber !== null && n.project?.number === targetNumber)) ?? null);
|
|
153
|
-
}
|
|
154
|
-
return nodes[0] ?? null;
|
|
155
|
-
}
|
|
156
|
-
function toProjectInfo(node) {
|
|
157
|
-
const proj = node.project;
|
|
158
|
-
if (!proj)
|
|
159
|
-
return null;
|
|
160
|
-
const options = proj.field?.options ?? [];
|
|
161
|
-
const status = node.fieldValueByName?.name ?? null;
|
|
162
|
-
const optionIndex = status ? options.findIndex((o) => o.name === status) : -1;
|
|
163
|
-
const option = optionIndex >= 0 ? options[optionIndex] : undefined;
|
|
164
|
-
return {
|
|
165
|
-
projectTitle: proj.title ?? "(untitled project)",
|
|
166
|
-
projectUrl: proj.url ?? "",
|
|
167
|
-
projectNumber: proj.number ?? 0,
|
|
168
|
-
status,
|
|
169
|
-
statusColor: option?.color
|
|
170
|
-
? (PROJECT_STATUS_COLORS[option.color] ?? "yellow")
|
|
171
|
-
: status
|
|
172
|
-
? "yellow"
|
|
173
|
-
: "gray",
|
|
174
|
-
statusOrder: optionIndex >= 0 ? optionIndex : STATUS_ORDER_UNSET,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Fetches, in a single GraphQL round-trip, which Projects v2 board (and
|
|
179
|
-
* Status value) each open assigned issue belongs to. Returns an empty map
|
|
180
|
-
* when the lookup fails — the dashboard then renders an ungrouped list.
|
|
181
|
-
*/
|
|
182
|
-
async function fetchProjectAssignments(statusFieldName, configuredUrl) {
|
|
183
|
-
const result = new Map();
|
|
184
|
-
const repo = await getRepoFullName();
|
|
185
|
-
if (!repo)
|
|
186
|
-
return result;
|
|
187
|
-
// The Status field name is interpolated into the query (not a variable)
|
|
188
|
-
// because it appears as a field argument; escape embedded quotes.
|
|
189
|
-
const escapedField = statusFieldName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
190
|
-
const searchQuery = `repo:${repo} is:issue is:open assignee:@me`.replace(/"/g, '\\"');
|
|
191
|
-
const query = `query {
|
|
192
|
-
search(query: "${searchQuery}", type: ISSUE, first: ${ISSUE_LIST_LIMIT}) {
|
|
193
|
-
nodes {
|
|
194
|
-
... on Issue {
|
|
195
|
-
number
|
|
196
|
-
projectItems(first: 10, includeArchived: false) {
|
|
197
|
-
nodes {
|
|
198
|
-
project {
|
|
199
|
-
title
|
|
200
|
-
number
|
|
201
|
-
url
|
|
202
|
-
field(name: "${escapedField}") {
|
|
203
|
-
... on ProjectV2SingleSelectField {
|
|
204
|
-
options { name color }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
fieldValueByName(name: "${escapedField}") {
|
|
209
|
-
... on ProjectV2ItemFieldSingleSelectValue { name }
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}`;
|
|
217
|
-
const raw = (await ghGraphql(query));
|
|
218
|
-
const nodes = raw?.data?.search?.nodes;
|
|
219
|
-
if (!Array.isArray(nodes))
|
|
220
|
-
return result;
|
|
221
|
-
for (const node of nodes) {
|
|
222
|
-
if (typeof node?.number !== "number")
|
|
223
|
-
continue;
|
|
224
|
-
const items = node.projectItems?.nodes ?? [];
|
|
225
|
-
const picked = pickProjectNode(items, configuredUrl);
|
|
226
|
-
if (!picked)
|
|
227
|
-
continue;
|
|
228
|
-
const info = toProjectInfo(picked);
|
|
229
|
-
if (info)
|
|
230
|
-
result.set(node.number, info);
|
|
231
|
-
}
|
|
232
|
-
return result;
|
|
233
|
-
}
|
|
234
66
|
/**
|
|
235
67
|
* Orders the flat issue list so issues are contiguous by project, then by
|
|
236
68
|
* Status (board column order), then newest issue first. The dashboard derives
|
|
@@ -260,7 +92,14 @@ function sortGroupedIssues(issues, configuredUrl) {
|
|
|
260
92
|
return a.project.statusOrder - b.project.statusOrder;
|
|
261
93
|
}
|
|
262
94
|
}
|
|
263
|
-
|
|
95
|
+
// Newest-first for issues — id is a numeric-or-prefixed string. Numeric
|
|
96
|
+
// compare falls back to localeCompare for non-numeric ids (Plane's
|
|
97
|
+
// "BACK-100" form).
|
|
98
|
+
const an = Number(a.issue.id);
|
|
99
|
+
const bn = Number(b.issue.id);
|
|
100
|
+
if (Number.isFinite(an) && Number.isFinite(bn))
|
|
101
|
+
return bn - an;
|
|
102
|
+
return b.issue.id.localeCompare(a.issue.id);
|
|
264
103
|
});
|
|
265
104
|
}
|
|
266
105
|
/**
|
|
@@ -269,7 +108,8 @@ function sortGroupedIssues(issues, configuredUrl) {
|
|
|
269
108
|
* `r` refresh — cheap because all the per-worktree probes are local.
|
|
270
109
|
*/
|
|
271
110
|
export async function loadDashboard(repoRoot) {
|
|
272
|
-
const
|
|
111
|
+
const provider = createProvider(repoRoot);
|
|
112
|
+
const issues = await provider.listAssignedIssues();
|
|
273
113
|
if (!issues)
|
|
274
114
|
return null;
|
|
275
115
|
const worktreesByIssue = buildWorktreeIndex(repoRoot);
|
|
@@ -288,24 +128,28 @@ export async function loadDashboard(repoRoot) {
|
|
|
288
128
|
if (pr)
|
|
289
129
|
prByBranch.set(w.branch, pr);
|
|
290
130
|
});
|
|
291
|
-
// Project membership comes from a single
|
|
292
|
-
// the per-branch PR probes so neither blocks the other.
|
|
131
|
+
// Project membership comes from the provider in a single call; fetch it
|
|
132
|
+
// alongside the per-branch PR probes so neither blocks the other.
|
|
293
133
|
const [, projectByIssue] = await Promise.all([
|
|
294
134
|
Promise.all(prFetches),
|
|
295
|
-
fetchProjectAssignments(
|
|
135
|
+
provider.fetchProjectAssignments(),
|
|
296
136
|
]);
|
|
137
|
+
// Provider signals total failure (vs no projects configured) with null —
|
|
138
|
+
// treat as a partial load failure so the caller's resilient refresh
|
|
139
|
+
// keeps the last-good state instead of regressing to a flat list.
|
|
140
|
+
if (projectByIssue === null)
|
|
141
|
+
return null;
|
|
297
142
|
const enriched = issues.map((issue) => {
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const sessionId = metadata.issues[issueId]?.session_id;
|
|
143
|
+
const worktreeRaw = worktreesByIssue.get(issue.id) ?? null;
|
|
144
|
+
const sessionId = metadata.issues[issue.id]?.session_id;
|
|
301
145
|
const worktree = worktreeRaw ? { ...worktreeRaw, sessionId } : null;
|
|
302
146
|
const pr = worktree && worktree.branch ? (prByBranch.get(worktree.branch) ?? null) : null;
|
|
303
147
|
return {
|
|
304
148
|
issue,
|
|
305
149
|
worktree,
|
|
306
|
-
session: readSessionState(repoRoot,
|
|
150
|
+
session: readSessionState(repoRoot, issue.id),
|
|
307
151
|
pr,
|
|
308
|
-
project: projectByIssue.get(issue.
|
|
152
|
+
project: projectByIssue.get(issue.id) ?? null,
|
|
309
153
|
};
|
|
310
154
|
});
|
|
311
155
|
return sortGroupedIssues(enriched, configuredUrl);
|
package/dist/lib/gh.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin shell helpers around the `gh` CLI. These are deliberately
|
|
3
|
+
* provider-agnostic — even when mintree's issue provider is Plane, `gh` is
|
|
4
|
+
* still used to look up PR status for worktree branches, so doctor and a
|
|
5
|
+
* couple of dashboard surfaces need to know whether `gh` is reachable.
|
|
6
|
+
*
|
|
7
|
+
* The GitHub issue / Project v2 logic lives in `providers/github.ts` and
|
|
8
|
+
* uses these helpers internally; the Plane provider doesn't touch them.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ghCliAvailable(): Promise<boolean>;
|
|
11
|
+
export declare function getGhUserLogin(): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Returns "owner/name" for the GitHub repo of the current working directory,
|
|
14
|
+
* or null if not a GitHub repo / `gh` can't reach the API.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRepoFullName(): Promise<string | null>;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin shell helpers around the `gh` CLI. These are deliberately
|
|
3
|
+
* provider-agnostic — even when mintree's issue provider is Plane, `gh` is
|
|
4
|
+
* still used to look up PR status for worktree branches, so doctor and a
|
|
5
|
+
* couple of dashboard surfaces need to know whether `gh` is reachable.
|
|
6
|
+
*
|
|
7
|
+
* The GitHub issue / Project v2 logic lives in `providers/github.ts` and
|
|
8
|
+
* uses these helpers internally; the Plane provider doesn't touch them.
|
|
9
|
+
*/
|
|
1
10
|
import { tryExec } from "./exec.js";
|
|
2
11
|
export async function ghCliAvailable() {
|
|
3
12
|
const out = await tryExec("which gh");
|
package/dist/lib/metadata.d.ts
CHANGED
|
@@ -8,10 +8,25 @@ export type ProjectMeta = {
|
|
|
8
8
|
inProgressOption?: string;
|
|
9
9
|
protectedStatuses?: string[];
|
|
10
10
|
};
|
|
11
|
+
export type ProviderKind = "github" | "plane";
|
|
12
|
+
export type PlaneProjectRef = {
|
|
13
|
+
id: string;
|
|
14
|
+
identifier: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
};
|
|
17
|
+
export type PlaneMeta = {
|
|
18
|
+
apiUrl?: string;
|
|
19
|
+
workspaceSlug: string;
|
|
20
|
+
projects: PlaneProjectRef[];
|
|
21
|
+
inProgressStateName?: string;
|
|
22
|
+
protectedStateGroups?: string[];
|
|
23
|
+
};
|
|
11
24
|
export type Metadata = {
|
|
12
25
|
version: 1;
|
|
26
|
+
provider?: ProviderKind;
|
|
13
27
|
issues: Record<string, IssueMeta>;
|
|
14
28
|
project?: ProjectMeta;
|
|
29
|
+
plane?: PlaneMeta;
|
|
15
30
|
};
|
|
16
31
|
export declare function readMetadata(repoRoot: string): Metadata;
|
|
17
32
|
export declare function writeMetadata(repoRoot: string, data: Metadata): void;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { getMetadataPath } from "./git.js";
|
|
3
3
|
const EMPTY = { version: 1, issues: {} };
|
|
4
|
+
function sanitizeProvider(raw) {
|
|
5
|
+
if (raw === "github" || raw === "plane")
|
|
6
|
+
return raw;
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
function sanitizePlaneProject(raw) {
|
|
10
|
+
if (typeof raw !== "object" || raw === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
const r = raw;
|
|
13
|
+
if (typeof r["id"] !== "string" || r["id"].length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof r["identifier"] !== "string" || r["identifier"].length === 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
const out = { id: r["id"], identifier: r["identifier"] };
|
|
18
|
+
if (typeof r["name"] === "string" && r["name"].length > 0)
|
|
19
|
+
out.name = r["name"];
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function sanitizePlane(raw) {
|
|
23
|
+
if (typeof raw !== "object" || raw === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const r = raw;
|
|
26
|
+
if (typeof r["workspaceSlug"] !== "string" || r["workspaceSlug"].length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
const projectsRaw = Array.isArray(r["projects"]) ? r["projects"] : [];
|
|
29
|
+
const projects = [];
|
|
30
|
+
for (const p of projectsRaw) {
|
|
31
|
+
const sanitized = sanitizePlaneProject(p);
|
|
32
|
+
if (sanitized)
|
|
33
|
+
projects.push(sanitized);
|
|
34
|
+
}
|
|
35
|
+
const out = {
|
|
36
|
+
workspaceSlug: r["workspaceSlug"],
|
|
37
|
+
projects,
|
|
38
|
+
};
|
|
39
|
+
if (typeof r["apiUrl"] === "string" && r["apiUrl"].length > 0)
|
|
40
|
+
out.apiUrl = r["apiUrl"];
|
|
41
|
+
if (typeof r["inProgressStateName"] === "string" && r["inProgressStateName"].length > 0) {
|
|
42
|
+
out.inProgressStateName = r["inProgressStateName"];
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(r["protectedStateGroups"])) {
|
|
45
|
+
const arr = r["protectedStateGroups"].filter((v) => typeof v === "string" && v.length > 0);
|
|
46
|
+
if (arr.length > 0)
|
|
47
|
+
out.protectedStateGroups = arr;
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
4
51
|
function sanitizeProject(raw) {
|
|
5
52
|
if (typeof raw !== "object" || raw === null)
|
|
6
53
|
return undefined;
|
|
@@ -30,12 +77,16 @@ export function readMetadata(repoRoot) {
|
|
|
30
77
|
if (typeof parsed !== "object" || parsed === null)
|
|
31
78
|
return { ...EMPTY, issues: {} };
|
|
32
79
|
const project = sanitizeProject(parsed.project);
|
|
80
|
+
const provider = sanitizeProvider(parsed.provider);
|
|
81
|
+
const plane = sanitizePlane(parsed.plane);
|
|
33
82
|
return {
|
|
34
83
|
version: 1,
|
|
35
84
|
issues: typeof parsed.issues === "object" && parsed.issues !== null
|
|
36
85
|
? parsed.issues
|
|
37
86
|
: {},
|
|
87
|
+
...(provider ? { provider } : {}),
|
|
38
88
|
...(project ? { project } : {}),
|
|
89
|
+
...(plane ? { plane } : {}),
|
|
39
90
|
};
|
|
40
91
|
}
|
|
41
92
|
catch {
|
package/dist/lib/pr.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `gh pr list` helpers. The dashboard, `worktree list --pr`, and
|
|
3
|
+
* `worktree clean` all need to look up the PR status of a branch, with the
|
|
4
|
+
* same `gh pr list --head <branch>` shape. Centralising them here avoids
|
|
5
|
+
* three copies of the shell-quote + JSON-parse dance going out of sync.
|
|
6
|
+
*
|
|
7
|
+
* PR detection stays gh-only even when the issue provider is Plane —
|
|
8
|
+
* mintree's worktree branches live on GitHub, and Plane has no concept of
|
|
9
|
+
* git PRs. Callers that aren't sure whether `gh` is available pass through
|
|
10
|
+
* `tryExec`-style failures as `null`, so the dashboard degrades to "no PR"
|
|
11
|
+
* rows instead of erroring.
|
|
12
|
+
*/
|
|
13
|
+
export type PrState = "OPEN" | "CLOSED" | "MERGED";
|
|
14
|
+
export type PrInfo = {
|
|
15
|
+
number: number;
|
|
16
|
+
state: PrState;
|
|
17
|
+
url?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Looks up the most recent PR for a branch (any state). Returns null when
|
|
21
|
+
* there's no PR or `gh` can't reach the API. `withUrl` controls whether the
|
|
22
|
+
* URL field is requested — dashboard wants it for display, list/clean don't.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fetchPrForBranch(branch: string, { withUrl }?: {
|
|
25
|
+
withUrl?: boolean;
|
|
26
|
+
}): Promise<PrInfo | null>;
|
package/dist/lib/pr.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `gh pr list` helpers. The dashboard, `worktree list --pr`, and
|
|
3
|
+
* `worktree clean` all need to look up the PR status of a branch, with the
|
|
4
|
+
* same `gh pr list --head <branch>` shape. Centralising them here avoids
|
|
5
|
+
* three copies of the shell-quote + JSON-parse dance going out of sync.
|
|
6
|
+
*
|
|
7
|
+
* PR detection stays gh-only even when the issue provider is Plane —
|
|
8
|
+
* mintree's worktree branches live on GitHub, and Plane has no concept of
|
|
9
|
+
* git PRs. Callers that aren't sure whether `gh` is available pass through
|
|
10
|
+
* `tryExec`-style failures as `null`, so the dashboard degrades to "no PR"
|
|
11
|
+
* rows instead of erroring.
|
|
12
|
+
*/
|
|
13
|
+
import { tryExec } from "./exec.js";
|
|
14
|
+
function shQuote(value) {
|
|
15
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
16
|
+
}
|
|
17
|
+
function normaliseState(s) {
|
|
18
|
+
const u = s.toUpperCase();
|
|
19
|
+
if (u === "OPEN" || u === "CLOSED" || u === "MERGED")
|
|
20
|
+
return u;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Looks up the most recent PR for a branch (any state). Returns null when
|
|
25
|
+
* there's no PR or `gh` can't reach the API. `withUrl` controls whether the
|
|
26
|
+
* URL field is requested — dashboard wants it for display, list/clean don't.
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchPrForBranch(branch, { withUrl = true } = {}) {
|
|
29
|
+
const fields = withUrl ? "number,state,url" : "number,state";
|
|
30
|
+
const out = await tryExec(`gh pr list --head ${shQuote(branch)} --state all --json ${fields} --limit 1 2>/dev/null`);
|
|
31
|
+
if (!out)
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const arr = JSON.parse(out);
|
|
35
|
+
if (!Array.isArray(arr) || arr.length === 0 || !arr[0])
|
|
36
|
+
return null;
|
|
37
|
+
const first = arr[0];
|
|
38
|
+
const state = normaliseState(first.state);
|
|
39
|
+
if (!state)
|
|
40
|
+
return null;
|
|
41
|
+
const result = { number: first.number, state };
|
|
42
|
+
if (withUrl && first.url)
|
|
43
|
+
result.url = first.url;
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GithubProvider — implements IssueProvider against GitHub Issues + Projects
|
|
3
|
+
* v2 via the `gh` CLI. All the GraphQL plumbing that previously lived in
|
|
4
|
+
* dashboard.ts (project assignment lookup) and githubProject.ts (status
|
|
5
|
+
* transition) is consolidated here so the rest of mintree can talk to issues
|
|
6
|
+
* through a stable, provider-agnostic interface.
|
|
7
|
+
*
|
|
8
|
+
* Stays gh-CLI-driven (not raw octokit) because gh transparently handles
|
|
9
|
+
* auth tokens, scope refresh, and the user's preferred login — mintree's
|
|
10
|
+
* doctor already validates that flow, and not having a second auth path
|
|
11
|
+
* means there's only one thing to break.
|
|
12
|
+
*/
|
|
13
|
+
import type { IssueId, IssueProjectInfo, IssueProvider, ProviderIssue, TransitionResult } from "./types.js";
|
|
14
|
+
export declare class GithubProvider implements IssueProvider {
|
|
15
|
+
private readonly repoRoot;
|
|
16
|
+
readonly kind: "github";
|
|
17
|
+
constructor(repoRoot: string);
|
|
18
|
+
private readProjectConfig;
|
|
19
|
+
listAssignedIssues(): Promise<ProviderIssue[] | null>;
|
|
20
|
+
fetchProjectAssignments(): Promise<Map<IssueId, IssueProjectInfo> | null>;
|
|
21
|
+
transitionIssueToInProgress(issueId: IssueId): Promise<TransitionResult>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns the gh CLI token scopes for github.com, or null when `gh` can't be
|
|
25
|
+
* called / the user isn't authenticated. `gh auth status` writes the scopes
|
|
26
|
+
* line to stderr; we capture both streams and grep for it.
|
|
27
|
+
*
|
|
28
|
+
* Kept as a standalone export (not part of IssueProvider) because it's
|
|
29
|
+
* consumed by doctor for the Project v2 scope row — a doctor-side concern,
|
|
30
|
+
* not part of the runtime issue flow.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getGhTokenScopes(): Promise<string[] | null>;
|
|
33
|
+
export declare function hasProjectScope(scopes: string[]): boolean;
|