@cliangdev/flux-plugin 0.3.0 → 0.3.1-dev.bdbaeae
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/bin/install.cjs +2 -2
- package/commands/dashboard.md +1 -1
- package/package.json +3 -1
- package/src/server/adapters/factory.ts +6 -28
- package/src/server/adapters/github/__tests__/criteria-deps.test.ts +579 -0
- package/src/server/adapters/github/__tests__/documents-stats.test.ts +789 -0
- package/src/server/adapters/github/__tests__/epic-task-crud.test.ts +1072 -0
- package/src/server/adapters/github/__tests__/foundation.test.ts +537 -0
- package/src/server/adapters/github/__tests__/index-store.test.ts +319 -0
- package/src/server/adapters/github/__tests__/prd-crud.test.ts +836 -0
- package/src/server/adapters/github/adapter.ts +1552 -0
- package/src/server/adapters/github/client.ts +33 -0
- package/src/server/adapters/github/config.ts +59 -0
- package/src/server/adapters/github/helpers/criteria.ts +157 -0
- package/src/server/adapters/github/helpers/index-store.ts +75 -0
- package/src/server/adapters/github/helpers/meta.ts +26 -0
- package/src/server/adapters/github/index.ts +5 -0
- package/src/server/adapters/github/mappers/epic.ts +21 -0
- package/src/server/adapters/github/mappers/index.ts +15 -0
- package/src/server/adapters/github/mappers/prd.ts +50 -0
- package/src/server/adapters/github/mappers/task.ts +37 -0
- package/src/server/adapters/github/types.ts +27 -0
- package/src/server/adapters/types.ts +1 -1
- package/src/server/index.ts +2 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +6 -0
- package/src/server/tools/__tests__/z-configure-github.test.ts +509 -0
- package/src/server/tools/__tests__/z-init-project.test.ts +168 -0
- package/src/server/tools/configure-github.ts +411 -0
- package/src/server/tools/index.ts +2 -1
- package/src/server/tools/init-project.ts +26 -12
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { graphql } from "@octokit/graphql";
|
|
2
|
+
import { Octokit } from "@octokit/rest";
|
|
3
|
+
import type { GitHubConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export class GitHubClient {
|
|
6
|
+
readonly rest: Octokit;
|
|
7
|
+
private gql: ReturnType<typeof graphql.defaults>;
|
|
8
|
+
|
|
9
|
+
constructor(config: GitHubConfig) {
|
|
10
|
+
this.rest = new Octokit({ auth: config.token });
|
|
11
|
+
this.gql = graphql.defaults({
|
|
12
|
+
headers: {
|
|
13
|
+
authorization: `token ${config.token}`,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async graphql<T>(
|
|
19
|
+
query: string,
|
|
20
|
+
variables?: Record<string, unknown>,
|
|
21
|
+
): Promise<T> {
|
|
22
|
+
try {
|
|
23
|
+
return await this.gql<T>(query, variables);
|
|
24
|
+
} catch (error: any) {
|
|
25
|
+
if (error.status === 401 || error.status === 403) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`GitHub API auth error: token may lack 'repo' scope (HTTP ${error.status})`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { config } from "../../config.js";
|
|
3
|
+
import type { GitHubConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function githubConfigExists(): boolean {
|
|
6
|
+
if (!existsSync(config.projectJsonPath)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const content = readFileSync(config.projectJsonPath, "utf-8");
|
|
12
|
+
const project = JSON.parse(content);
|
|
13
|
+
return project?.adapter?.type === "github";
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadGithubConfig(): GitHubConfig {
|
|
20
|
+
if (!existsSync(config.projectJsonPath)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"GitHub config not found: project.json missing. Run project init first.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const content = readFileSync(config.projectJsonPath, "utf-8");
|
|
27
|
+
const project = JSON.parse(content);
|
|
28
|
+
const cfg = project?.adapter?.config;
|
|
29
|
+
|
|
30
|
+
if (!cfg || typeof cfg !== "object") {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"GitHub config not found in project.json: adapter.config is missing.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!cfg.token || typeof cfg.token !== "string") {
|
|
37
|
+
throw new Error("Invalid GitHub config: token is required");
|
|
38
|
+
}
|
|
39
|
+
if (!cfg.owner || typeof cfg.owner !== "string") {
|
|
40
|
+
throw new Error("Invalid GitHub config: owner is required");
|
|
41
|
+
}
|
|
42
|
+
if (!cfg.repo || typeof cfg.repo !== "string") {
|
|
43
|
+
throw new Error("Invalid GitHub config: repo is required");
|
|
44
|
+
}
|
|
45
|
+
if (!cfg.projectId || typeof cfg.projectId !== "string") {
|
|
46
|
+
throw new Error("Invalid GitHub config: projectId is required");
|
|
47
|
+
}
|
|
48
|
+
if (!cfg.refPrefix || typeof cfg.refPrefix !== "string") {
|
|
49
|
+
throw new Error("Invalid GitHub config: refPrefix is required");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
token: cfg.token,
|
|
54
|
+
owner: cfg.owner,
|
|
55
|
+
repo: cfg.repo,
|
|
56
|
+
projectId: cfg.projectId,
|
|
57
|
+
refPrefix: cfg.refPrefix,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface ParsedCriterion {
|
|
4
|
+
id: string;
|
|
5
|
+
text: string;
|
|
6
|
+
isMet: boolean;
|
|
7
|
+
lineIndex: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function generateCriteriaId(text: string): string {
|
|
11
|
+
const hash = createHash("sha256").update(text.trim()).digest("hex");
|
|
12
|
+
return `ac_${hash.substring(0, 8)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseCriteriaFromDescription(
|
|
16
|
+
description: string | undefined,
|
|
17
|
+
): ParsedCriterion[] {
|
|
18
|
+
if (!description) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = description.split("\n");
|
|
23
|
+
|
|
24
|
+
let acSectionStart = -1;
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const trimmedLine = lines[i].trim().toLowerCase();
|
|
27
|
+
if (
|
|
28
|
+
trimmedLine.includes("acceptance criteria") &&
|
|
29
|
+
(trimmedLine.startsWith("#") || trimmedLine.startsWith("**"))
|
|
30
|
+
) {
|
|
31
|
+
acSectionStart = i;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const criteria: ParsedCriterion[] = [];
|
|
37
|
+
let inACSection = acSectionStart === -1;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i];
|
|
41
|
+
const trimmedLine = line.trim().toLowerCase();
|
|
42
|
+
|
|
43
|
+
if (i === acSectionStart) {
|
|
44
|
+
inACSection = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (acSectionStart !== -1 && i > acSectionStart && inACSection) {
|
|
49
|
+
if (
|
|
50
|
+
trimmedLine.startsWith("#") ||
|
|
51
|
+
(trimmedLine.startsWith("**") && trimmedLine.endsWith("**"))
|
|
52
|
+
) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (inACSection) {
|
|
58
|
+
const checkboxMatch = line.match(/^[\s]*[-*]\s*\[([ xX])\]\s*(.+)$/);
|
|
59
|
+
if (checkboxMatch) {
|
|
60
|
+
const isChecked = checkboxMatch[1].toLowerCase() === "x";
|
|
61
|
+
const text = checkboxMatch[2].trim();
|
|
62
|
+
|
|
63
|
+
criteria.push({
|
|
64
|
+
id: generateCriteriaId(text),
|
|
65
|
+
text,
|
|
66
|
+
isMet: isChecked,
|
|
67
|
+
lineIndex: i,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return criteria;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function updateCriterionInDescription(
|
|
77
|
+
description: string,
|
|
78
|
+
criteriaId: string,
|
|
79
|
+
isMet: boolean,
|
|
80
|
+
): string {
|
|
81
|
+
const lines = description.split("\n");
|
|
82
|
+
const criteria = parseCriteriaFromDescription(description);
|
|
83
|
+
|
|
84
|
+
const criterion = criteria.find((c) => c.id === criteriaId);
|
|
85
|
+
if (!criterion) {
|
|
86
|
+
throw new Error(`Criterion not found: ${criteriaId}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const line = lines[criterion.lineIndex];
|
|
90
|
+
const newCheckbox = isMet ? "[x]" : "[ ]";
|
|
91
|
+
const updatedLine = line.replace(/\[([ xX])\]/, newCheckbox);
|
|
92
|
+
lines[criterion.lineIndex] = updatedLine;
|
|
93
|
+
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function addCriterionToDescription(
|
|
98
|
+
description: string | undefined,
|
|
99
|
+
criterionText: string,
|
|
100
|
+
): string {
|
|
101
|
+
if (!description) {
|
|
102
|
+
return `## Acceptance Criteria\n\n- [ ] ${criterionText}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const lines = description.split("\n");
|
|
106
|
+
|
|
107
|
+
let acSectionStart = -1;
|
|
108
|
+
let acSectionEnd = lines.length;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const trimmedLine = lines[i].trim().toLowerCase();
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
trimmedLine.includes("acceptance criteria") &&
|
|
115
|
+
(trimmedLine.startsWith("#") || trimmedLine.startsWith("**"))
|
|
116
|
+
) {
|
|
117
|
+
acSectionStart = i;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (acSectionStart !== -1 && i > acSectionStart) {
|
|
122
|
+
const line = lines[i].trim();
|
|
123
|
+
if (
|
|
124
|
+
line.startsWith("#") ||
|
|
125
|
+
(line.startsWith("**") && line.endsWith("**"))
|
|
126
|
+
) {
|
|
127
|
+
acSectionEnd = i;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let insertIndex = -1;
|
|
134
|
+
if (acSectionStart !== -1) {
|
|
135
|
+
for (let i = acSectionEnd - 1; i > acSectionStart; i--) {
|
|
136
|
+
if (lines[i].match(/^[\s]*[-*]\s*\[([ xX])\]/)) {
|
|
137
|
+
insertIndex = i + 1;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (insertIndex === -1) {
|
|
142
|
+
insertIndex = acSectionStart + 1;
|
|
143
|
+
while (insertIndex < lines.length && lines[insertIndex].trim() === "") {
|
|
144
|
+
insertIndex++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push("## Acceptance Criteria");
|
|
150
|
+
lines.push("");
|
|
151
|
+
insertIndex = lines.length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lines.splice(insertIndex, 0, `- [ ] ${criterionText}`);
|
|
155
|
+
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { GitHubClient } from "../client.js";
|
|
2
|
+
|
|
3
|
+
export type RefIndex = Record<string, number>;
|
|
4
|
+
|
|
5
|
+
const INDEX_PATH = ".flux/github-index.json";
|
|
6
|
+
|
|
7
|
+
async function fetchCurrentFile(
|
|
8
|
+
client: GitHubClient,
|
|
9
|
+
owner: string,
|
|
10
|
+
repo: string,
|
|
11
|
+
): Promise<{ sha: string; content: string } | null> {
|
|
12
|
+
try {
|
|
13
|
+
const response = await client.rest.repos.getContent({
|
|
14
|
+
owner,
|
|
15
|
+
repo,
|
|
16
|
+
path: INDEX_PATH,
|
|
17
|
+
});
|
|
18
|
+
const data = response.data as {
|
|
19
|
+
sha: string;
|
|
20
|
+
content: string;
|
|
21
|
+
encoding: string;
|
|
22
|
+
};
|
|
23
|
+
return { sha: data.sha, content: data.content };
|
|
24
|
+
} catch (err: any) {
|
|
25
|
+
if (err.status === 404) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readIndex(
|
|
33
|
+
client: GitHubClient,
|
|
34
|
+
owner: string,
|
|
35
|
+
repo: string,
|
|
36
|
+
): Promise<RefIndex> {
|
|
37
|
+
const file = await fetchCurrentFile(client, owner, repo);
|
|
38
|
+
if (file === null) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
const json = Buffer.from(file.content, "base64").toString("utf-8");
|
|
42
|
+
return JSON.parse(json) as RefIndex;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function writeIndex(
|
|
46
|
+
client: GitHubClient,
|
|
47
|
+
owner: string,
|
|
48
|
+
repo: string,
|
|
49
|
+
index: RefIndex,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const existing = await fetchCurrentFile(client, owner, repo);
|
|
52
|
+
|
|
53
|
+
const params: Record<string, unknown> = {
|
|
54
|
+
owner,
|
|
55
|
+
repo,
|
|
56
|
+
path: INDEX_PATH,
|
|
57
|
+
message: "chore: update flux ref index",
|
|
58
|
+
content: Buffer.from(JSON.stringify(index, null, 2)).toString("base64"),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (existing !== null) {
|
|
62
|
+
params.sha = existing.sha;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await client.rest.repos.createOrUpdateFileContents(params as any);
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
if (err.status === 409) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Index conflict: another operation modified the index concurrently. Retry the operation.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface FluxMeta {
|
|
2
|
+
ref: string;
|
|
3
|
+
tag?: string;
|
|
4
|
+
prd_ref?: string;
|
|
5
|
+
epic_ref?: string;
|
|
6
|
+
priority?: string;
|
|
7
|
+
dependencies?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function encodeMeta(meta: FluxMeta): string {
|
|
11
|
+
return `<!-- flux-meta\n${JSON.stringify(meta)}\n-->`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function decodeMeta(body: string): FluxMeta | null {
|
|
15
|
+
const match = body.match(/<!--\s*flux-meta\n([\s\S]*?)\n-->/);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(match[1]) as FluxMeta;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function extractDescription(body: string): string {
|
|
25
|
+
return body.replace(/<!--\s*flux-meta[\s\S]*?-->/g, "").trim();
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EpicStatus } from "../../types.js";
|
|
2
|
+
import { GITHUB_LABELS } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const STATUS_LABEL_MAP: Record<EpicStatus, string | null> = {
|
|
5
|
+
PENDING: null,
|
|
6
|
+
IN_PROGRESS: GITHUB_LABELS.STATUS_IN_PROGRESS,
|
|
7
|
+
COMPLETED: null,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function epicStatusToLabel(status: EpicStatus): string | null {
|
|
11
|
+
return STATUS_LABEL_MAP[status];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function labelToEpicStatus(
|
|
15
|
+
labels: string[],
|
|
16
|
+
isClosed: boolean,
|
|
17
|
+
): EpicStatus {
|
|
18
|
+
if (isClosed) return "COMPLETED";
|
|
19
|
+
if (labels.includes(GITHUB_LABELS.STATUS_IN_PROGRESS)) return "IN_PROGRESS";
|
|
20
|
+
return "PENDING";
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { epicStatusToLabel, labelToEpicStatus } from "./epic.js";
|
|
2
|
+
export {
|
|
3
|
+
getAllStatusLabels,
|
|
4
|
+
labelToPrdStatus,
|
|
5
|
+
labelToTag,
|
|
6
|
+
prdStatusToLabel,
|
|
7
|
+
tagToLabel,
|
|
8
|
+
} from "./prd.js";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
labelToPriority,
|
|
12
|
+
labelToTaskStatus,
|
|
13
|
+
priorityToLabel,
|
|
14
|
+
taskStatusToLabel,
|
|
15
|
+
} from "./task.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PrdStatus } from "../../types.js";
|
|
2
|
+
import { GITHUB_LABELS } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const STATUS_LABEL_MAP: Record<PrdStatus, string | null> = {
|
|
5
|
+
DRAFT: GITHUB_LABELS.STATUS_DRAFT,
|
|
6
|
+
PENDING_REVIEW: GITHUB_LABELS.STATUS_PENDING_REVIEW,
|
|
7
|
+
REVIEWED: GITHUB_LABELS.STATUS_REVIEWED,
|
|
8
|
+
APPROVED: GITHUB_LABELS.STATUS_APPROVED,
|
|
9
|
+
BREAKDOWN_READY: GITHUB_LABELS.STATUS_BREAKDOWN_READY,
|
|
10
|
+
COMPLETED: GITHUB_LABELS.STATUS_COMPLETED,
|
|
11
|
+
ARCHIVED: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const ALL_STATUS_LABELS = new Set(
|
|
15
|
+
Object.values(STATUS_LABEL_MAP).filter(Boolean) as string[],
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export function prdStatusToLabel(status: PrdStatus): string | null {
|
|
19
|
+
return STATUS_LABEL_MAP[status];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function labelToPrdStatus(
|
|
23
|
+
labels: string[],
|
|
24
|
+
isClosed: boolean,
|
|
25
|
+
): PrdStatus {
|
|
26
|
+
if (isClosed) return "ARCHIVED";
|
|
27
|
+
|
|
28
|
+
if (labels.includes(GITHUB_LABELS.STATUS_COMPLETED)) return "COMPLETED";
|
|
29
|
+
if (labels.includes(GITHUB_LABELS.STATUS_BREAKDOWN_READY))
|
|
30
|
+
return "BREAKDOWN_READY";
|
|
31
|
+
if (labels.includes(GITHUB_LABELS.STATUS_APPROVED)) return "APPROVED";
|
|
32
|
+
if (labels.includes(GITHUB_LABELS.STATUS_REVIEWED)) return "REVIEWED";
|
|
33
|
+
if (labels.includes(GITHUB_LABELS.STATUS_PENDING_REVIEW))
|
|
34
|
+
return "PENDING_REVIEW";
|
|
35
|
+
|
|
36
|
+
return "DRAFT";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function tagToLabel(tag: string): string {
|
|
40
|
+
return `${GITHUB_LABELS.TAG_PREFIX}${tag}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function labelToTag(labels: string[]): string | undefined {
|
|
44
|
+
const tagLabel = labels.find((l) => l.startsWith(GITHUB_LABELS.TAG_PREFIX));
|
|
45
|
+
return tagLabel ? tagLabel.slice(GITHUB_LABELS.TAG_PREFIX.length) : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getAllStatusLabels(): string[] {
|
|
49
|
+
return [...ALL_STATUS_LABELS];
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Priority, TaskStatus } from "../../types.js";
|
|
2
|
+
import { GITHUB_LABELS } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const TASK_STATUS_LABEL_MAP: Record<TaskStatus, string | null> = {
|
|
5
|
+
PENDING: null,
|
|
6
|
+
IN_PROGRESS: GITHUB_LABELS.STATUS_IN_PROGRESS,
|
|
7
|
+
COMPLETED: null,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function taskStatusToLabel(status: TaskStatus): string | null {
|
|
11
|
+
return TASK_STATUS_LABEL_MAP[status];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function labelToTaskStatus(
|
|
15
|
+
labels: string[],
|
|
16
|
+
isClosed: boolean,
|
|
17
|
+
): TaskStatus {
|
|
18
|
+
if (isClosed) return "COMPLETED";
|
|
19
|
+
if (labels.includes(GITHUB_LABELS.STATUS_IN_PROGRESS)) return "IN_PROGRESS";
|
|
20
|
+
return "PENDING";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PRIORITY_LABEL_MAP: Record<Priority, string> = {
|
|
24
|
+
LOW: GITHUB_LABELS.PRIORITY_LOW,
|
|
25
|
+
MEDIUM: GITHUB_LABELS.PRIORITY_MEDIUM,
|
|
26
|
+
HIGH: GITHUB_LABELS.PRIORITY_HIGH,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function priorityToLabel(priority: Priority): string {
|
|
30
|
+
return PRIORITY_LABEL_MAP[priority];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function labelToPriority(labels: string[]): Priority {
|
|
34
|
+
if (labels.includes(GITHUB_LABELS.PRIORITY_HIGH)) return "HIGH";
|
|
35
|
+
if (labels.includes(GITHUB_LABELS.PRIORITY_LOW)) return "LOW";
|
|
36
|
+
return "MEDIUM";
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface GitHubConfig {
|
|
2
|
+
token: string;
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
refPrefix: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const GITHUB_LABELS = {
|
|
10
|
+
ENTITY_PRD: "flux:prd",
|
|
11
|
+
ENTITY_EPIC: "flux:epic",
|
|
12
|
+
ENTITY_TASK: "flux:task",
|
|
13
|
+
|
|
14
|
+
STATUS_DRAFT: "status:draft",
|
|
15
|
+
STATUS_PENDING_REVIEW: "status:pending-review",
|
|
16
|
+
STATUS_REVIEWED: "status:reviewed",
|
|
17
|
+
STATUS_APPROVED: "status:approved",
|
|
18
|
+
STATUS_BREAKDOWN_READY: "status:breakdown-ready",
|
|
19
|
+
STATUS_COMPLETED: "status:completed",
|
|
20
|
+
STATUS_IN_PROGRESS: "status:in-progress",
|
|
21
|
+
|
|
22
|
+
PRIORITY_LOW: "priority:low",
|
|
23
|
+
PRIORITY_MEDIUM: "priority:medium",
|
|
24
|
+
PRIORITY_HIGH: "priority:high",
|
|
25
|
+
|
|
26
|
+
TAG_PREFIX: "tag:",
|
|
27
|
+
} as const;
|
|
@@ -288,6 +288,6 @@ export interface CascadeResult {
|
|
|
288
288
|
* Adapter configuration stored in project.json
|
|
289
289
|
*/
|
|
290
290
|
export interface AdapterConfig {
|
|
291
|
-
type: "local" | "specflux" | "linear" | "notion";
|
|
291
|
+
type: "local" | "specflux" | "linear" | "notion" | "github";
|
|
292
292
|
config?: Record<string, unknown>;
|
|
293
293
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fluxProjectExists, initDb } from "./db/index.js";
|
|
|
5
5
|
import {
|
|
6
6
|
addCriteriaTool,
|
|
7
7
|
addDependencyTool,
|
|
8
|
+
configureGithubTool,
|
|
8
9
|
configureLinearTool,
|
|
9
10
|
createEpicTool,
|
|
10
11
|
createPrdTool,
|
|
@@ -50,6 +51,7 @@ const tools: ToolDefinition[] = [
|
|
|
50
51
|
getStatsTool,
|
|
51
52
|
getVersionTool,
|
|
52
53
|
// Configuration tools
|
|
54
|
+
configureGithubTool,
|
|
53
55
|
configureLinearTool,
|
|
54
56
|
// Display tools
|
|
55
57
|
renderStatusTool,
|
|
@@ -84,6 +84,12 @@ describe("MCP Interface", () => {
|
|
|
84
84
|
expect(toolOption?.params?.vision).toBeDefined();
|
|
85
85
|
expect(toolOption?.params?.adapter).toBeDefined();
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
test("adapter param includes github option", () => {
|
|
89
|
+
const error = createProjectNotInitializedError("/cwd", "/root");
|
|
90
|
+
const toolOption = error.setup.options.find((o) => o.method === "tool");
|
|
91
|
+
expect(toolOption?.params?.adapter).toContain("github");
|
|
92
|
+
});
|
|
87
93
|
});
|
|
88
94
|
|
|
89
95
|
describe("registerTools", () => {
|