@snipcodeit/mgw 0.1.3 → 0.2.1
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/commands/init.md +115 -5
- package/commands/issues.md +63 -1
- package/commands/milestone.md +43 -0
- package/commands/review.md +289 -149
- package/commands/status.md +95 -1
- package/commands/workflows/gsd.md +70 -0
- package/completions/mgw.bash +112 -0
- package/completions/mgw.fish +99 -0
- package/completions/mgw.zsh +142 -0
- package/dist/bin/mgw.cjs +113 -29
- package/dist/index-CXfe9U4l.cjs +1818 -0
- package/dist/lib/index.cjs +109 -8
- package/package.json +6 -1
- package/dist/claude-Dk1oVsaG.cjs +0 -622
|
@@ -0,0 +1,1818 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var require$$0 = require('fs');
|
|
4
|
+
var require$$1 = require('path');
|
|
5
|
+
var require$$0$1 = require('child_process');
|
|
6
|
+
var require$$0$2 = require('events');
|
|
7
|
+
|
|
8
|
+
function getDefaultExportFromCjs(x) {
|
|
9
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
var state;
|
|
13
|
+
var hasRequiredState;
|
|
14
|
+
|
|
15
|
+
function requireState () {
|
|
16
|
+
if (hasRequiredState) return state;
|
|
17
|
+
hasRequiredState = 1;
|
|
18
|
+
const fs = require$$0;
|
|
19
|
+
const path = require$$1;
|
|
20
|
+
function getMgwDir() {
|
|
21
|
+
return path.join(process.cwd(), ".mgw");
|
|
22
|
+
}
|
|
23
|
+
function getActiveDir() {
|
|
24
|
+
return path.join(getMgwDir(), "active");
|
|
25
|
+
}
|
|
26
|
+
function getCompletedDir() {
|
|
27
|
+
return path.join(getMgwDir(), "completed");
|
|
28
|
+
}
|
|
29
|
+
function loadProjectState() {
|
|
30
|
+
const filePath = path.join(getMgwDir(), "project.json");
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeProjectState(state) {
|
|
39
|
+
const mgwDir = getMgwDir();
|
|
40
|
+
if (!fs.existsSync(mgwDir)) {
|
|
41
|
+
fs.mkdirSync(mgwDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
const filePath = path.join(mgwDir, "project.json");
|
|
44
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
45
|
+
}
|
|
46
|
+
function loadActiveIssue(number) {
|
|
47
|
+
const activeDir = getActiveDir();
|
|
48
|
+
if (!fs.existsSync(activeDir)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const prefix = String(number) + "-";
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = fs.readdirSync(activeDir);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const match = entries.find(
|
|
59
|
+
(f) => f.startsWith(prefix) && f.endsWith(".json")
|
|
60
|
+
);
|
|
61
|
+
if (!match) return null;
|
|
62
|
+
try {
|
|
63
|
+
const raw = fs.readFileSync(path.join(activeDir, match), "utf-8");
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function mergeProjectState(newMilestones, newPhaseMap, newCurrentMilestone, activeGsdMilestone) {
|
|
70
|
+
const existing = loadProjectState();
|
|
71
|
+
if (!existing) {
|
|
72
|
+
throw new Error("No existing project state found. Cannot merge without a project.json.");
|
|
73
|
+
}
|
|
74
|
+
existing.milestones = (existing.milestones || []).concat(newMilestones);
|
|
75
|
+
existing.phase_map = Object.assign({}, newPhaseMap, existing.phase_map);
|
|
76
|
+
if (activeGsdMilestone !== void 0 && activeGsdMilestone !== null) {
|
|
77
|
+
existing.active_gsd_milestone = activeGsdMilestone;
|
|
78
|
+
} else {
|
|
79
|
+
if (!existing.active_gsd_milestone) {
|
|
80
|
+
existing.current_milestone = newCurrentMilestone;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
writeProjectState(existing);
|
|
84
|
+
return existing;
|
|
85
|
+
}
|
|
86
|
+
function migrateProjectState() {
|
|
87
|
+
const existing = loadProjectState();
|
|
88
|
+
if (!existing) return null;
|
|
89
|
+
let changed = false;
|
|
90
|
+
if (!existing.hasOwnProperty("active_gsd_milestone")) {
|
|
91
|
+
existing.active_gsd_milestone = null;
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
for (const m of existing.milestones || []) {
|
|
95
|
+
if (!m.hasOwnProperty("gsd_milestone_id")) {
|
|
96
|
+
m.gsd_milestone_id = null;
|
|
97
|
+
changed = true;
|
|
98
|
+
}
|
|
99
|
+
if (!m.hasOwnProperty("gsd_state")) {
|
|
100
|
+
m.gsd_state = null;
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
if (!m.hasOwnProperty("roadmap_archived_at")) {
|
|
104
|
+
m.roadmap_archived_at = null;
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (changed) {
|
|
109
|
+
writeProjectState(existing);
|
|
110
|
+
}
|
|
111
|
+
const activeDir = getActiveDir();
|
|
112
|
+
if (fs.existsSync(activeDir)) {
|
|
113
|
+
let entries;
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(activeDir);
|
|
116
|
+
} catch {
|
|
117
|
+
entries = [];
|
|
118
|
+
}
|
|
119
|
+
for (const file of entries) {
|
|
120
|
+
if (!file.endsWith(".json")) continue;
|
|
121
|
+
const filePath = path.join(activeDir, file);
|
|
122
|
+
let issueState;
|
|
123
|
+
try {
|
|
124
|
+
issueState = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let issueChanged = false;
|
|
129
|
+
if (!issueState.hasOwnProperty("retry_count")) {
|
|
130
|
+
issueState.retry_count = 0;
|
|
131
|
+
issueChanged = true;
|
|
132
|
+
}
|
|
133
|
+
if (!issueState.hasOwnProperty("dead_letter")) {
|
|
134
|
+
issueState.dead_letter = false;
|
|
135
|
+
issueChanged = true;
|
|
136
|
+
}
|
|
137
|
+
if (issueChanged) {
|
|
138
|
+
try {
|
|
139
|
+
fs.writeFileSync(filePath, JSON.stringify(issueState, null, 2), "utf-8");
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return existing;
|
|
146
|
+
}
|
|
147
|
+
function resolveActiveMilestoneIndex(state) {
|
|
148
|
+
if (!state) return -1;
|
|
149
|
+
if (state.active_gsd_milestone) {
|
|
150
|
+
const idx = (state.milestones || []).findIndex(
|
|
151
|
+
(m) => m.gsd_milestone_id === state.active_gsd_milestone
|
|
152
|
+
);
|
|
153
|
+
return idx;
|
|
154
|
+
}
|
|
155
|
+
if (typeof state.current_milestone === "number") {
|
|
156
|
+
return state.current_milestone - 1;
|
|
157
|
+
}
|
|
158
|
+
return -1;
|
|
159
|
+
}
|
|
160
|
+
state = {
|
|
161
|
+
getMgwDir,
|
|
162
|
+
getActiveDir,
|
|
163
|
+
getCompletedDir,
|
|
164
|
+
loadProjectState,
|
|
165
|
+
writeProjectState,
|
|
166
|
+
loadActiveIssue,
|
|
167
|
+
mergeProjectState,
|
|
168
|
+
migrateProjectState,
|
|
169
|
+
resolveActiveMilestoneIndex
|
|
170
|
+
};
|
|
171
|
+
return state;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var github;
|
|
175
|
+
var hasRequiredGithub;
|
|
176
|
+
|
|
177
|
+
function requireGithub () {
|
|
178
|
+
if (hasRequiredGithub) return github;
|
|
179
|
+
hasRequiredGithub = 1;
|
|
180
|
+
const { execSync } = require$$0$1;
|
|
181
|
+
function run(cmd) {
|
|
182
|
+
return execSync(cmd, {
|
|
183
|
+
encoding: "utf-8",
|
|
184
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
185
|
+
}).trim();
|
|
186
|
+
}
|
|
187
|
+
function getRepo() {
|
|
188
|
+
return run("gh repo view --json nameWithOwner -q .nameWithOwner");
|
|
189
|
+
}
|
|
190
|
+
function getIssue(number) {
|
|
191
|
+
const raw = run(
|
|
192
|
+
`gh issue view ${number} --json number,title,state,labels,milestone,assignees,body`
|
|
193
|
+
);
|
|
194
|
+
return JSON.parse(raw);
|
|
195
|
+
}
|
|
196
|
+
function listIssues(filters) {
|
|
197
|
+
const f = filters || {};
|
|
198
|
+
let cmd = "gh issue list --json number,title,state,labels,milestone,assignees,createdAt,url,body,comments";
|
|
199
|
+
if (f.label) cmd += ` --label ${JSON.stringify(f.label)}`;
|
|
200
|
+
if (f.milestone) cmd += ` --milestone ${JSON.stringify(f.milestone)}`;
|
|
201
|
+
if (f.assignee && f.assignee !== "all") cmd += ` --assignee ${JSON.stringify(f.assignee)}`;
|
|
202
|
+
if (f.state) cmd += ` --state ${f.state}`;
|
|
203
|
+
if (f.limit) cmd += ` --limit ${parseInt(f.limit, 10)}`;
|
|
204
|
+
const raw = run(cmd);
|
|
205
|
+
return JSON.parse(raw);
|
|
206
|
+
}
|
|
207
|
+
function getMilestone(number) {
|
|
208
|
+
const repo = getRepo();
|
|
209
|
+
const raw = run(`gh api repos/${repo}/milestones/${number}`);
|
|
210
|
+
return JSON.parse(raw);
|
|
211
|
+
}
|
|
212
|
+
function getRateLimit() {
|
|
213
|
+
const raw = run("gh api rate_limit");
|
|
214
|
+
const data = JSON.parse(raw);
|
|
215
|
+
const core = data.resources.core;
|
|
216
|
+
return {
|
|
217
|
+
remaining: core.remaining,
|
|
218
|
+
limit: core.limit,
|
|
219
|
+
reset: core.reset
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function closeMilestone(repo, number) {
|
|
223
|
+
const raw = run(
|
|
224
|
+
`gh api repos/${repo}/milestones/${number} --method PATCH -f state=closed`
|
|
225
|
+
);
|
|
226
|
+
return JSON.parse(raw);
|
|
227
|
+
}
|
|
228
|
+
function createRelease(repo, tag, title, opts) {
|
|
229
|
+
const o = opts || {};
|
|
230
|
+
let cmd = `gh release create ${JSON.stringify(tag)} --repo ${JSON.stringify(repo)} --title ${JSON.stringify(title)}`;
|
|
231
|
+
if (o.notes) cmd += ` --notes ${JSON.stringify(o.notes)}`;
|
|
232
|
+
if (o.draft) cmd += " --draft";
|
|
233
|
+
if (o.prerelease) cmd += " --prerelease";
|
|
234
|
+
return run(cmd);
|
|
235
|
+
}
|
|
236
|
+
function getProjectNodeId(owner, projectNumber) {
|
|
237
|
+
const userQuery = `'query($login: String!, $number: Int!) { user(login: $login) { projectV2(number: $number) { id } } }'`;
|
|
238
|
+
const orgQuery = `'query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { id } } }'`;
|
|
239
|
+
try {
|
|
240
|
+
const raw = run(
|
|
241
|
+
`gh api graphql -f query=${userQuery} -f login=${JSON.stringify(owner)} -F number=${projectNumber} --jq '.data.user.projectV2.id'`
|
|
242
|
+
);
|
|
243
|
+
if (raw && raw !== "null") return raw;
|
|
244
|
+
} catch (_) {
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const raw = run(
|
|
248
|
+
`gh api graphql -f query=${orgQuery} -f login=${JSON.stringify(owner)} -F number=${projectNumber} --jq '.data.organization.projectV2.id'`
|
|
249
|
+
);
|
|
250
|
+
if (raw && raw !== "null") return raw;
|
|
251
|
+
} catch (_) {
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
function findExistingBoard(owner, titlePattern) {
|
|
256
|
+
const pattern = titlePattern.toLowerCase();
|
|
257
|
+
try {
|
|
258
|
+
const raw = run(
|
|
259
|
+
`gh api graphql -f query='query($login: String!) { user(login: $login) { projectsV2(first: 20) { nodes { id number url title } } } }' -f login=${JSON.stringify(owner)} --jq '.data.user.projectsV2.nodes'`
|
|
260
|
+
);
|
|
261
|
+
const nodes = JSON.parse(raw);
|
|
262
|
+
const match = nodes.find((n) => n.title.toLowerCase().includes(pattern));
|
|
263
|
+
if (match) return { number: match.number, url: match.url, nodeId: match.id, title: match.title };
|
|
264
|
+
} catch (_) {
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const raw = run(
|
|
268
|
+
`gh api graphql -f query='query($login: String!) { organization(login: $login) { projectsV2(first: 20) { nodes { id number url title } } } }' -f login=${JSON.stringify(owner)} --jq '.data.organization.projectsV2.nodes'`
|
|
269
|
+
);
|
|
270
|
+
const nodes = JSON.parse(raw);
|
|
271
|
+
const match = nodes.find((n) => n.title.toLowerCase().includes(pattern));
|
|
272
|
+
if (match) return { number: match.number, url: match.url, nodeId: match.id, title: match.title };
|
|
273
|
+
} catch (_) {
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function getProjectFields(owner, projectNumber) {
|
|
278
|
+
const query = `'query($login: String!, $number: Int!) { user(login: $login) { projectV2(number: $number) { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } ... on ProjectV2Field { id name dataType } } } } } }'`;
|
|
279
|
+
const orgQuery = `'query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name options { id name } } ... on ProjectV2Field { id name dataType } } } } } }'`;
|
|
280
|
+
let raw;
|
|
281
|
+
try {
|
|
282
|
+
raw = run(`gh api graphql -f query=${query} -f login=${JSON.stringify(owner)} -F number=${projectNumber} --jq '.data.user.projectV2.fields.nodes'`);
|
|
283
|
+
} catch (_) {
|
|
284
|
+
try {
|
|
285
|
+
raw = run(`gh api graphql -f query=${orgQuery} -f login=${JSON.stringify(owner)} -F number=${projectNumber} --jq '.data.organization.projectV2.fields.nodes'`);
|
|
286
|
+
} catch (_2) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const nodes = JSON.parse(raw);
|
|
291
|
+
const fields = {};
|
|
292
|
+
const statusNode = nodes.find((n) => n.name === "Status" && n.options);
|
|
293
|
+
if (statusNode) {
|
|
294
|
+
const stageMap = {
|
|
295
|
+
"new": "New",
|
|
296
|
+
"triaged": "Triaged",
|
|
297
|
+
"needs-info": "Needs Info",
|
|
298
|
+
"needs-security-review": "Needs Security Review",
|
|
299
|
+
"discussing": "Discussing",
|
|
300
|
+
"approved": "Approved",
|
|
301
|
+
"planning": "Planning",
|
|
302
|
+
"executing": "Executing",
|
|
303
|
+
"verifying": "Verifying",
|
|
304
|
+
"pr-created": "PR Created",
|
|
305
|
+
"done": "Done",
|
|
306
|
+
"failed": "Failed",
|
|
307
|
+
"blocked": "Blocked"
|
|
308
|
+
};
|
|
309
|
+
const nameToId = Object.fromEntries(statusNode.options.map((o) => [o.name, o.id]));
|
|
310
|
+
fields.status = {
|
|
311
|
+
field_id: statusNode.id,
|
|
312
|
+
field_name: "Status",
|
|
313
|
+
type: "SINGLE_SELECT",
|
|
314
|
+
options: Object.fromEntries(
|
|
315
|
+
Object.entries(stageMap).map(([stage, label]) => [stage, nameToId[label] || ""])
|
|
316
|
+
)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const aiNode = nodes.find((n) => n.name === "AI Agent State" && !n.options);
|
|
320
|
+
if (aiNode) {
|
|
321
|
+
fields.ai_agent_state = { field_id: aiNode.id, field_name: "AI Agent State", type: "TEXT" };
|
|
322
|
+
}
|
|
323
|
+
const phaseNode = nodes.find((n) => n.name === "Phase" && !n.options);
|
|
324
|
+
if (phaseNode) {
|
|
325
|
+
fields.phase = { field_id: phaseNode.id, field_name: "Phase", type: "TEXT" };
|
|
326
|
+
}
|
|
327
|
+
const routeNode = nodes.find((n) => n.name === "GSD Route" && n.options);
|
|
328
|
+
if (routeNode) {
|
|
329
|
+
const routeMap = {
|
|
330
|
+
"gsd:quick": "quick",
|
|
331
|
+
"gsd:quick --full": "quick --full",
|
|
332
|
+
"gsd:plan-phase": "plan-phase",
|
|
333
|
+
"gsd:new-milestone": "new-milestone"
|
|
334
|
+
};
|
|
335
|
+
const nameToId = Object.fromEntries(routeNode.options.map((o) => [o.name, o.id]));
|
|
336
|
+
fields.gsd_route = {
|
|
337
|
+
field_id: routeNode.id,
|
|
338
|
+
field_name: "GSD Route",
|
|
339
|
+
type: "SINGLE_SELECT",
|
|
340
|
+
options: Object.fromEntries(
|
|
341
|
+
Object.entries(routeMap).map(([route, label]) => [route, nameToId[label] || ""])
|
|
342
|
+
)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const milestoneNode = nodes.find((n) => n.name === "Milestone");
|
|
346
|
+
if (milestoneNode) {
|
|
347
|
+
fields.milestone = {
|
|
348
|
+
field_id: milestoneNode.id,
|
|
349
|
+
field_name: "Milestone",
|
|
350
|
+
type: milestoneNode.dataType || (milestoneNode.options ? "SINGLE_SELECT" : "TEXT")
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return Object.keys(fields).length > 0 ? fields : null;
|
|
354
|
+
}
|
|
355
|
+
function createProject(owner, title) {
|
|
356
|
+
const raw = run(
|
|
357
|
+
`gh project create --owner ${JSON.stringify(owner)} --title ${JSON.stringify(title)} --format json`
|
|
358
|
+
);
|
|
359
|
+
const data = JSON.parse(raw);
|
|
360
|
+
return { number: data.number, url: data.url };
|
|
361
|
+
}
|
|
362
|
+
function addItemToProject(owner, projectNumber, issueUrl) {
|
|
363
|
+
return run(
|
|
364
|
+
`gh project item-add ${projectNumber} --owner ${JSON.stringify(owner)} --url ${JSON.stringify(issueUrl)}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
function postMilestoneStartAnnouncement(opts) {
|
|
368
|
+
const {
|
|
369
|
+
repo,
|
|
370
|
+
milestoneName,
|
|
371
|
+
boardUrl,
|
|
372
|
+
issues,
|
|
373
|
+
firstIssueNumber
|
|
374
|
+
} = opts;
|
|
375
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
376
|
+
const issueList = Array.isArray(issues) ? issues : [];
|
|
377
|
+
const issueRows = issueList.map((i) => {
|
|
378
|
+
const assignee = i.assignee ? `@${i.assignee}` : "\u2014";
|
|
379
|
+
return `| #${i.number} | ${i.title} | ${assignee} | \`${i.gsdRoute}\` |`;
|
|
380
|
+
}).join("\n");
|
|
381
|
+
const boardLine = boardUrl ? `**Board:** ${boardUrl}` : "**Board:** _(not configured)_";
|
|
382
|
+
const body = [
|
|
383
|
+
`> **MGW** \xB7 \`milestone-started\` \xB7 ${timestamp}`,
|
|
384
|
+
"",
|
|
385
|
+
`## Milestone Execution Started: ${milestoneName}`,
|
|
386
|
+
"",
|
|
387
|
+
boardLine,
|
|
388
|
+
"",
|
|
389
|
+
"### Issues in This Milestone",
|
|
390
|
+
"",
|
|
391
|
+
"| # | Title | Assignee | Route |",
|
|
392
|
+
"|---|-------|----------|-------|",
|
|
393
|
+
issueRows,
|
|
394
|
+
"",
|
|
395
|
+
`**${issueList.length} issue(s)** queued for autonomous execution. PRs will be posted on each issue as they complete.`,
|
|
396
|
+
"",
|
|
397
|
+
"---",
|
|
398
|
+
"*Auto-posted by MGW milestone orchestration*"
|
|
399
|
+
].join("\n");
|
|
400
|
+
const title = `[MGW] Milestone Started: ${milestoneName}`;
|
|
401
|
+
if (repo) {
|
|
402
|
+
try {
|
|
403
|
+
const [owner, repoName] = repo.split("/");
|
|
404
|
+
const repoMetaRaw = run(
|
|
405
|
+
`gh api graphql -f query='query { repository(owner: "${owner}", name: "${repoName}") { id discussionCategories(first: 20) { nodes { id name } } } }' --jq '.data.repository'`
|
|
406
|
+
);
|
|
407
|
+
const repoMeta = JSON.parse(repoMetaRaw);
|
|
408
|
+
const categories = repoMeta.discussionCategories && repoMeta.discussionCategories.nodes || [];
|
|
409
|
+
const announcements = categories.find((c) => c.name === "Announcements");
|
|
410
|
+
if (announcements) {
|
|
411
|
+
const repoId = repoMeta.id;
|
|
412
|
+
const categoryId = announcements.id;
|
|
413
|
+
const bodyEscaped = JSON.stringify(body);
|
|
414
|
+
const titleEscaped = JSON.stringify(title);
|
|
415
|
+
const resultRaw = run(
|
|
416
|
+
`gh api graphql -f query='mutation { createDiscussion(input: { repositoryId: ${JSON.stringify(repoId)}, categoryId: ${JSON.stringify(categoryId)}, title: ${titleEscaped}, body: ${bodyEscaped} }) { discussion { url } } }' --jq '.data.createDiscussion.discussion'`
|
|
417
|
+
);
|
|
418
|
+
const result = JSON.parse(resultRaw);
|
|
419
|
+
if (result && result.url) {
|
|
420
|
+
return { posted: true, method: "discussion", url: result.url };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch (_) {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (firstIssueNumber && repo) {
|
|
427
|
+
try {
|
|
428
|
+
run(
|
|
429
|
+
`gh issue comment ${firstIssueNumber} --repo ${JSON.stringify(repo)} --body ${JSON.stringify(body)}`
|
|
430
|
+
);
|
|
431
|
+
return { posted: true, method: "comment", url: null };
|
|
432
|
+
} catch (_) {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { posted: false, method: "none", url: null };
|
|
436
|
+
}
|
|
437
|
+
github = {
|
|
438
|
+
getRepo,
|
|
439
|
+
getIssue,
|
|
440
|
+
listIssues,
|
|
441
|
+
getMilestone,
|
|
442
|
+
getRateLimit,
|
|
443
|
+
closeMilestone,
|
|
444
|
+
createRelease,
|
|
445
|
+
getProjectNodeId,
|
|
446
|
+
findExistingBoard,
|
|
447
|
+
getProjectFields,
|
|
448
|
+
createProject,
|
|
449
|
+
addItemToProject,
|
|
450
|
+
postMilestoneStartAnnouncement
|
|
451
|
+
};
|
|
452
|
+
return github;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
var output;
|
|
456
|
+
var hasRequiredOutput;
|
|
457
|
+
|
|
458
|
+
function requireOutput () {
|
|
459
|
+
if (hasRequiredOutput) return output;
|
|
460
|
+
hasRequiredOutput = 1;
|
|
461
|
+
const IS_TTY = process.stdout.isTTY === true;
|
|
462
|
+
const IS_CI = Boolean(process.env.CI);
|
|
463
|
+
const USE_COLOR = IS_TTY && !IS_CI && !process.env.NO_COLOR;
|
|
464
|
+
const COLORS = {
|
|
465
|
+
reset: "\x1B[0m",
|
|
466
|
+
red: "\x1B[31m",
|
|
467
|
+
green: "\x1B[32m",
|
|
468
|
+
yellow: "\x1B[33m",
|
|
469
|
+
blue: "\x1B[34m",
|
|
470
|
+
cyan: "\x1B[36m",
|
|
471
|
+
bold: "\x1B[1m",
|
|
472
|
+
dim: "\x1B[2m"
|
|
473
|
+
};
|
|
474
|
+
function colorize(text, colorCode) {
|
|
475
|
+
if (!USE_COLOR) return text;
|
|
476
|
+
return `${colorCode}${text}${COLORS.reset}`;
|
|
477
|
+
}
|
|
478
|
+
function statusLine(icon, message) {
|
|
479
|
+
if (USE_COLOR) {
|
|
480
|
+
return `${icon} ${message}`;
|
|
481
|
+
}
|
|
482
|
+
return message;
|
|
483
|
+
}
|
|
484
|
+
function log(message) {
|
|
485
|
+
console.log(message);
|
|
486
|
+
}
|
|
487
|
+
function error(message) {
|
|
488
|
+
const prefix = colorize("error:", COLORS.red);
|
|
489
|
+
console.error(`${prefix} ${message}`);
|
|
490
|
+
}
|
|
491
|
+
function verbose(message, opts) {
|
|
492
|
+
if (opts && (opts.verbose || opts.debug)) {
|
|
493
|
+
console.log(colorize(` ${message}`, COLORS.dim));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function debug(message, opts) {
|
|
497
|
+
if (opts && opts.debug) {
|
|
498
|
+
console.log(colorize(`[debug] ${message}`, COLORS.dim));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function formatJson(data) {
|
|
502
|
+
return JSON.stringify(data, null, 2);
|
|
503
|
+
}
|
|
504
|
+
output = {
|
|
505
|
+
IS_TTY,
|
|
506
|
+
IS_CI,
|
|
507
|
+
USE_COLOR,
|
|
508
|
+
COLORS,
|
|
509
|
+
colorize,
|
|
510
|
+
statusLine,
|
|
511
|
+
log,
|
|
512
|
+
error,
|
|
513
|
+
verbose,
|
|
514
|
+
debug,
|
|
515
|
+
formatJson
|
|
516
|
+
};
|
|
517
|
+
return output;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
var claude;
|
|
521
|
+
var hasRequiredClaude;
|
|
522
|
+
|
|
523
|
+
function requireClaude () {
|
|
524
|
+
if (hasRequiredClaude) return claude;
|
|
525
|
+
hasRequiredClaude = 1;
|
|
526
|
+
const { execSync, spawn } = require$$0$1;
|
|
527
|
+
const path = require$$1;
|
|
528
|
+
const fs = require$$0;
|
|
529
|
+
function getCommandsDir() {
|
|
530
|
+
const dir = path.join(__dirname, "..", "commands");
|
|
531
|
+
if (!fs.existsSync(dir)) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Commands directory not found at: ${dir}
|
|
534
|
+
This may indicate a corrupted installation. Try reinstalling mgw.`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
return dir;
|
|
538
|
+
}
|
|
539
|
+
function assertClaudeAvailable() {
|
|
540
|
+
try {
|
|
541
|
+
execSync("claude --version", {
|
|
542
|
+
encoding: "utf-8",
|
|
543
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
544
|
+
});
|
|
545
|
+
} catch (err) {
|
|
546
|
+
if (err.code === "ENOENT") {
|
|
547
|
+
console.error(
|
|
548
|
+
"Error: claude CLI is not installed.\n\nInstall it with:\n npm install -g @anthropic-ai/claude-code\n\nThen run:\n claude login"
|
|
549
|
+
);
|
|
550
|
+
} else {
|
|
551
|
+
console.error(
|
|
552
|
+
"Error: claude CLI check failed.\nEnsure claude is installed and on your PATH."
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
execSync("claude auth status", {
|
|
559
|
+
encoding: "utf-8",
|
|
560
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
561
|
+
});
|
|
562
|
+
} catch {
|
|
563
|
+
console.error(
|
|
564
|
+
"Error: claude CLI is not authenticated.\n\nRun:\n claude login\n\nThen retry your command."
|
|
565
|
+
);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function invokeClaude(commandFile, userPrompt, opts) {
|
|
570
|
+
const o = opts || {};
|
|
571
|
+
const args = ["-p"];
|
|
572
|
+
if (commandFile) {
|
|
573
|
+
args.push("--system-prompt-file", commandFile);
|
|
574
|
+
}
|
|
575
|
+
if (o.model) {
|
|
576
|
+
args.push("--model", o.model);
|
|
577
|
+
}
|
|
578
|
+
if (o.json) {
|
|
579
|
+
args.push("--output-format", "json");
|
|
580
|
+
}
|
|
581
|
+
if (userPrompt) {
|
|
582
|
+
args.push(userPrompt);
|
|
583
|
+
}
|
|
584
|
+
if (o.dryRun) {
|
|
585
|
+
console.log("Would invoke: claude " + args.join(" "));
|
|
586
|
+
return Promise.resolve({ exitCode: 0, output: "" });
|
|
587
|
+
}
|
|
588
|
+
return new Promise((resolve, reject) => {
|
|
589
|
+
const stdio = o.quiet ? ["pipe", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
|
|
590
|
+
const child = spawn("claude", args, { stdio });
|
|
591
|
+
let output = "";
|
|
592
|
+
if (o.quiet) {
|
|
593
|
+
child.stdout.on("data", (chunk) => {
|
|
594
|
+
output += chunk.toString();
|
|
595
|
+
});
|
|
596
|
+
child.stderr.on("data", (chunk) => {
|
|
597
|
+
output += chunk.toString();
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
child.on("error", (err) => {
|
|
601
|
+
if (err.code === "ENOENT") {
|
|
602
|
+
reject(new Error("claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code"));
|
|
603
|
+
} else {
|
|
604
|
+
reject(err);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
child.on("close", (code) => {
|
|
608
|
+
resolve({ exitCode: code || 0, output });
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
claude = {
|
|
613
|
+
assertClaudeAvailable,
|
|
614
|
+
invokeClaude,
|
|
615
|
+
getCommandsDir
|
|
616
|
+
};
|
|
617
|
+
return claude;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
var spinner;
|
|
621
|
+
var hasRequiredSpinner;
|
|
622
|
+
|
|
623
|
+
function requireSpinner () {
|
|
624
|
+
if (hasRequiredSpinner) return spinner;
|
|
625
|
+
hasRequiredSpinner = 1;
|
|
626
|
+
const { IS_TTY, IS_CI, USE_COLOR, COLORS } = requireOutput();
|
|
627
|
+
const FRAMES = ["|", "/", "-", "\\"];
|
|
628
|
+
const SUPPORTS_SPINNER = IS_TTY && !IS_CI;
|
|
629
|
+
function createSpinner(stage) {
|
|
630
|
+
let frameIndex = 0;
|
|
631
|
+
let intervalId = null;
|
|
632
|
+
let startTime = null;
|
|
633
|
+
let lastLineLength = 0;
|
|
634
|
+
function clearLine() {
|
|
635
|
+
if (!SUPPORTS_SPINNER) return;
|
|
636
|
+
process.stdout.write("\r" + " ".repeat(lastLineLength) + "\r");
|
|
637
|
+
}
|
|
638
|
+
function render() {
|
|
639
|
+
const frame = FRAMES[frameIndex % FRAMES.length];
|
|
640
|
+
frameIndex++;
|
|
641
|
+
const elapsed = startTime ? Math.floor((Date.now() - startTime) / 1e3) : 0;
|
|
642
|
+
const elapsedStr = elapsed > 0 ? ` (${elapsed}s)` : "";
|
|
643
|
+
let line;
|
|
644
|
+
if (USE_COLOR) {
|
|
645
|
+
line = `${COLORS.cyan}${frame}${COLORS.reset} ${COLORS.bold}${stage}${COLORS.reset}${COLORS.dim}${elapsedStr}${COLORS.reset}`;
|
|
646
|
+
} else {
|
|
647
|
+
line = `${frame} ${stage}${elapsedStr}`;
|
|
648
|
+
}
|
|
649
|
+
if (SUPPORTS_SPINNER) {
|
|
650
|
+
clearLine();
|
|
651
|
+
process.stdout.write(line);
|
|
652
|
+
lastLineLength = `${frame} ${stage}${elapsedStr}`.length;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function start(label) {
|
|
656
|
+
if (label) stage = label;
|
|
657
|
+
startTime = Date.now();
|
|
658
|
+
if (SUPPORTS_SPINNER) {
|
|
659
|
+
render();
|
|
660
|
+
intervalId = setInterval(render, 100);
|
|
661
|
+
} else {
|
|
662
|
+
process.stdout.write(`[mgw] ${stage}...
|
|
663
|
+
`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function succeed(message) {
|
|
667
|
+
_stop();
|
|
668
|
+
const text = message || stage;
|
|
669
|
+
if (USE_COLOR) {
|
|
670
|
+
process.stdout.write(`${COLORS.green}\u2713${COLORS.reset} ${text}
|
|
671
|
+
`);
|
|
672
|
+
} else {
|
|
673
|
+
process.stdout.write(`[done] ${text}
|
|
674
|
+
`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function fail(message) {
|
|
678
|
+
_stop();
|
|
679
|
+
const text = message || stage;
|
|
680
|
+
if (USE_COLOR) {
|
|
681
|
+
process.stdout.write(`${COLORS.red}\u2717${COLORS.reset} ${text}
|
|
682
|
+
`);
|
|
683
|
+
} else {
|
|
684
|
+
process.stdout.write(`[fail] ${text}
|
|
685
|
+
`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function stop() {
|
|
689
|
+
_stop();
|
|
690
|
+
}
|
|
691
|
+
function _stop() {
|
|
692
|
+
if (intervalId !== null) {
|
|
693
|
+
clearInterval(intervalId);
|
|
694
|
+
intervalId = null;
|
|
695
|
+
}
|
|
696
|
+
clearLine();
|
|
697
|
+
}
|
|
698
|
+
return { start, succeed, fail, stop };
|
|
699
|
+
}
|
|
700
|
+
async function withSpinner(stage, fn, opts) {
|
|
701
|
+
const o = opts || {};
|
|
702
|
+
const spinner = createSpinner(stage);
|
|
703
|
+
spinner.start();
|
|
704
|
+
try {
|
|
705
|
+
const result = await fn();
|
|
706
|
+
spinner.succeed(o.successMessage || stage);
|
|
707
|
+
return result;
|
|
708
|
+
} catch (err) {
|
|
709
|
+
spinner.fail(o.failMessage || `${stage} failed`);
|
|
710
|
+
throw err;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
spinner = {
|
|
714
|
+
createSpinner,
|
|
715
|
+
withSpinner,
|
|
716
|
+
SUPPORTS_SPINNER
|
|
717
|
+
};
|
|
718
|
+
return spinner;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
var renderer;
|
|
722
|
+
var hasRequiredRenderer;
|
|
723
|
+
|
|
724
|
+
function requireRenderer () {
|
|
725
|
+
if (hasRequiredRenderer) return renderer;
|
|
726
|
+
hasRequiredRenderer = 1;
|
|
727
|
+
let blessed;
|
|
728
|
+
try {
|
|
729
|
+
blessed = require("neo-blessed");
|
|
730
|
+
} catch (_e) {
|
|
731
|
+
try {
|
|
732
|
+
blessed = require("blessed");
|
|
733
|
+
} catch (_e2) {
|
|
734
|
+
blessed = null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function createRenderer() {
|
|
738
|
+
if (!blessed) {
|
|
739
|
+
return createNoopRenderer();
|
|
740
|
+
}
|
|
741
|
+
return createBlessedRenderer();
|
|
742
|
+
}
|
|
743
|
+
function createBlessedRenderer() {
|
|
744
|
+
const screen = blessed.screen({
|
|
745
|
+
smartCSR: true,
|
|
746
|
+
title: "MGW Issues",
|
|
747
|
+
cursor: {
|
|
748
|
+
artificial: true,
|
|
749
|
+
shape: "line",
|
|
750
|
+
blink: true,
|
|
751
|
+
color: null
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
const header = blessed.box({
|
|
755
|
+
top: 0,
|
|
756
|
+
left: 0,
|
|
757
|
+
width: "100%",
|
|
758
|
+
height: 1,
|
|
759
|
+
content: " MGW ISSUES [/] search [f] filter [j/k] scroll [Enter] select [q] quit [?] help",
|
|
760
|
+
style: {
|
|
761
|
+
fg: "brightwhite",
|
|
762
|
+
bg: "brightblack",
|
|
763
|
+
bold: true
|
|
764
|
+
},
|
|
765
|
+
tags: false
|
|
766
|
+
});
|
|
767
|
+
const searchBar = blessed.box({
|
|
768
|
+
top: 1,
|
|
769
|
+
left: 0,
|
|
770
|
+
width: "100%",
|
|
771
|
+
height: 1,
|
|
772
|
+
content: "Search: ",
|
|
773
|
+
style: {
|
|
774
|
+
fg: "brightwhite",
|
|
775
|
+
bg: "black"
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
const issueList = blessed.list({
|
|
779
|
+
top: 2,
|
|
780
|
+
left: 0,
|
|
781
|
+
width: "60%",
|
|
782
|
+
height: "100%-5",
|
|
783
|
+
// header + search + filterbar + status
|
|
784
|
+
style: {
|
|
785
|
+
fg: "brightwhite",
|
|
786
|
+
bg: "black",
|
|
787
|
+
selected: {
|
|
788
|
+
fg: "black",
|
|
789
|
+
bg: "magenta"
|
|
790
|
+
},
|
|
791
|
+
border: {
|
|
792
|
+
fg: "cyan"
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
border: { type: "line" },
|
|
796
|
+
scrollable: true,
|
|
797
|
+
keys: true,
|
|
798
|
+
mouse: true,
|
|
799
|
+
items: []
|
|
800
|
+
});
|
|
801
|
+
const detailPane = blessed.box({
|
|
802
|
+
top: 2,
|
|
803
|
+
left: "60%",
|
|
804
|
+
width: "40%",
|
|
805
|
+
height: "100%-5",
|
|
806
|
+
style: {
|
|
807
|
+
fg: "brightwhite",
|
|
808
|
+
bg: "black",
|
|
809
|
+
border: {
|
|
810
|
+
fg: "cyan"
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
border: { type: "line" },
|
|
814
|
+
scrollable: true,
|
|
815
|
+
keys: true,
|
|
816
|
+
mouse: true,
|
|
817
|
+
content: "",
|
|
818
|
+
tags: false
|
|
819
|
+
});
|
|
820
|
+
const filterPane = blessed.box({
|
|
821
|
+
top: 2,
|
|
822
|
+
left: "60%",
|
|
823
|
+
width: "40%",
|
|
824
|
+
height: "100%-5",
|
|
825
|
+
label: " Filters [f] close [j/k] move [Space] toggle [c] clear ",
|
|
826
|
+
style: {
|
|
827
|
+
fg: "brightwhite",
|
|
828
|
+
bg: "black",
|
|
829
|
+
border: {
|
|
830
|
+
fg: "yellow",
|
|
831
|
+
bold: true
|
|
832
|
+
},
|
|
833
|
+
label: {
|
|
834
|
+
fg: "yellow"
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
border: { type: "line" },
|
|
838
|
+
scrollable: true,
|
|
839
|
+
keys: false,
|
|
840
|
+
mouse: false,
|
|
841
|
+
content: "",
|
|
842
|
+
tags: false,
|
|
843
|
+
hidden: true
|
|
844
|
+
});
|
|
845
|
+
const filterBar = blessed.box({
|
|
846
|
+
bottom: 1,
|
|
847
|
+
left: 0,
|
|
848
|
+
width: "100%",
|
|
849
|
+
height: 1,
|
|
850
|
+
content: " Filter: none",
|
|
851
|
+
style: {
|
|
852
|
+
fg: "yellow",
|
|
853
|
+
bg: "black"
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
const statusBar = blessed.box({
|
|
857
|
+
bottom: 0,
|
|
858
|
+
left: 0,
|
|
859
|
+
width: "100%",
|
|
860
|
+
height: 1,
|
|
861
|
+
content: " 0 issues",
|
|
862
|
+
style: {
|
|
863
|
+
fg: "brightwhite",
|
|
864
|
+
bg: "brightblack"
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
const helpOverlay = blessed.box({
|
|
868
|
+
top: "center",
|
|
869
|
+
left: "center",
|
|
870
|
+
width: 56,
|
|
871
|
+
height: 22,
|
|
872
|
+
label: " Keyboard Shortcuts ",
|
|
873
|
+
hidden: true,
|
|
874
|
+
border: { type: "line" },
|
|
875
|
+
style: {
|
|
876
|
+
fg: "brightwhite",
|
|
877
|
+
bg: "black",
|
|
878
|
+
border: { fg: "cyan" },
|
|
879
|
+
label: { fg: "cyan" }
|
|
880
|
+
},
|
|
881
|
+
content: [
|
|
882
|
+
"",
|
|
883
|
+
" Navigation",
|
|
884
|
+
" j / \u2193 Scroll down",
|
|
885
|
+
" k / \u2191 Scroll up",
|
|
886
|
+
" g / Home Jump to top",
|
|
887
|
+
" G / End Jump to bottom",
|
|
888
|
+
" PgDn Page down",
|
|
889
|
+
" PgUp Page up",
|
|
890
|
+
" Tab Cycle pane focus",
|
|
891
|
+
" Shift+Tab Cycle pane reverse",
|
|
892
|
+
"",
|
|
893
|
+
" Actions",
|
|
894
|
+
" / Open search",
|
|
895
|
+
" f Open/close filter pane",
|
|
896
|
+
" Space Toggle filter item",
|
|
897
|
+
" c Clear all filters",
|
|
898
|
+
" Enter Select issue",
|
|
899
|
+
" q / Esc Quit / close overlay",
|
|
900
|
+
" Ctrl+C Force quit",
|
|
901
|
+
" ? Show this help",
|
|
902
|
+
""
|
|
903
|
+
].join("\n"),
|
|
904
|
+
tags: false
|
|
905
|
+
});
|
|
906
|
+
screen.append(header);
|
|
907
|
+
screen.append(searchBar);
|
|
908
|
+
screen.append(issueList);
|
|
909
|
+
screen.append(detailPane);
|
|
910
|
+
screen.append(filterPane);
|
|
911
|
+
screen.append(filterBar);
|
|
912
|
+
screen.append(statusBar);
|
|
913
|
+
screen.append(helpOverlay);
|
|
914
|
+
let lastState = null;
|
|
915
|
+
let helpVisible = false;
|
|
916
|
+
function _formatListItem(issue) {
|
|
917
|
+
const num = `#${issue.number}`.padEnd(6);
|
|
918
|
+
const title = (issue.title || "").slice(0, 45);
|
|
919
|
+
return `${num} ${title}`;
|
|
920
|
+
}
|
|
921
|
+
function _formatDetail(issue) {
|
|
922
|
+
if (!issue) return "(no issue selected)";
|
|
923
|
+
const labels = (issue.labels || []).map((l) => typeof l === "object" ? l.name : l).join(", ") || "none";
|
|
924
|
+
const assignees = (issue.assignees || []).map((a) => typeof a === "object" ? a.login : a).join(", ") || "unassigned";
|
|
925
|
+
const milestone = issue.milestone ? typeof issue.milestone === "object" ? issue.milestone.title : issue.milestone : "none";
|
|
926
|
+
const commentCount = Array.isArray(issue.comments) ? issue.comments.length : issue.comments || 0;
|
|
927
|
+
return [
|
|
928
|
+
`#${issue.number} \u2014 ${issue.title}`,
|
|
929
|
+
"",
|
|
930
|
+
`Labels: ${labels}`,
|
|
931
|
+
`Assignees: ${assignees}`,
|
|
932
|
+
`Milestone: ${milestone}`,
|
|
933
|
+
`Comments: ${commentCount}`,
|
|
934
|
+
`URL: ${issue.url || ""}`,
|
|
935
|
+
"",
|
|
936
|
+
"\u2500".repeat(40),
|
|
937
|
+
"",
|
|
938
|
+
issue.body || "(no description)"
|
|
939
|
+
].join("\n");
|
|
940
|
+
}
|
|
941
|
+
function _formatFilterPane(filterState) {
|
|
942
|
+
if (!filterState) return "(no filters available)";
|
|
943
|
+
const lines = [];
|
|
944
|
+
const labelFocused = filterState.cursorSection === "labels";
|
|
945
|
+
lines.push(labelFocused ? " > Labels" : " Labels");
|
|
946
|
+
if (filterState.availableLabels.length === 0) {
|
|
947
|
+
lines.push(" (none)");
|
|
948
|
+
} else {
|
|
949
|
+
filterState.availableLabels.forEach((label, i) => {
|
|
950
|
+
const checked = filterState.activeLabels.has(label) ? "x" : " ";
|
|
951
|
+
const cursor = labelFocused && i === filterState.cursorIndex ? ">" : " ";
|
|
952
|
+
lines.push(` ${cursor}[${checked}] ${label}`);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
lines.push("");
|
|
956
|
+
const msFocused = filterState.cursorSection === "milestones";
|
|
957
|
+
lines.push(msFocused ? " > Milestones" : " Milestones");
|
|
958
|
+
if (filterState.availableMilestones.length === 0) {
|
|
959
|
+
lines.push(" (none)");
|
|
960
|
+
} else {
|
|
961
|
+
filterState.availableMilestones.forEach((ms, i) => {
|
|
962
|
+
const checked = filterState.activeMilestones.has(ms) ? "x" : " ";
|
|
963
|
+
const cursor = msFocused && i === filterState.cursorIndex ? ">" : " ";
|
|
964
|
+
lines.push(` ${cursor}[${checked}] ${ms}`);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
lines.push("");
|
|
968
|
+
const stateFocused = filterState.cursorSection === "state";
|
|
969
|
+
lines.push(stateFocused ? " > State" : " State");
|
|
970
|
+
const stateOptions = ["open", "closed", "all"];
|
|
971
|
+
stateOptions.forEach((opt, i) => {
|
|
972
|
+
const selected = filterState.activeState === opt ? "*" : " ";
|
|
973
|
+
const cursor = stateFocused && i === filterState.cursorIndex ? ">" : " ";
|
|
974
|
+
lines.push(` ${cursor}(${selected}) ${opt}`);
|
|
975
|
+
});
|
|
976
|
+
return lines.join("\n");
|
|
977
|
+
}
|
|
978
|
+
function _formatFilterBar(filterState) {
|
|
979
|
+
if (!filterState || filterState.isEmpty) {
|
|
980
|
+
return " Filter: none [f] open filter pane";
|
|
981
|
+
}
|
|
982
|
+
const parts = [];
|
|
983
|
+
if (filterState.activeLabels.size > 0) {
|
|
984
|
+
parts.push(`Labels: ${Array.from(filterState.activeLabels).join(", ")}`);
|
|
985
|
+
}
|
|
986
|
+
if (filterState.activeMilestones.size > 0) {
|
|
987
|
+
parts.push(`Milestone: ${Array.from(filterState.activeMilestones).join(", ")}`);
|
|
988
|
+
}
|
|
989
|
+
if (filterState.activeState !== "open") {
|
|
990
|
+
parts.push(`State: ${filterState.activeState}`);
|
|
991
|
+
}
|
|
992
|
+
return ` Filter: ${parts.join(" | ")} [f] edit [c] clear`;
|
|
993
|
+
}
|
|
994
|
+
function render(state) {
|
|
995
|
+
lastState = state;
|
|
996
|
+
const { filtered, selectedIndex, query, focusPane, filterState } = state;
|
|
997
|
+
helpVisible = Boolean(state.helpVisible);
|
|
998
|
+
const items = filtered.map(_formatListItem);
|
|
999
|
+
issueList.setItems(items);
|
|
1000
|
+
if (filtered.length > 0) {
|
|
1001
|
+
issueList.select(selectedIndex);
|
|
1002
|
+
}
|
|
1003
|
+
const cursor = focusPane === "search" ? "\u2588" : "";
|
|
1004
|
+
searchBar.setContent(`Search: ${query}${cursor}`);
|
|
1005
|
+
if (focusPane === "filter") {
|
|
1006
|
+
detailPane.hide();
|
|
1007
|
+
filterPane.setContent(_formatFilterPane(filterState || null));
|
|
1008
|
+
filterPane.show();
|
|
1009
|
+
} else {
|
|
1010
|
+
filterPane.hide();
|
|
1011
|
+
const selectedIssue = filtered[selectedIndex] || null;
|
|
1012
|
+
detailPane.setContent(_formatDetail(selectedIssue));
|
|
1013
|
+
detailPane.show();
|
|
1014
|
+
}
|
|
1015
|
+
filterBar.setContent(_formatFilterBar(filterState || null));
|
|
1016
|
+
const filterActive = filterState && !filterState.isEmpty;
|
|
1017
|
+
const filterHint = filterActive ? " [c] clear filters" : "";
|
|
1018
|
+
const searchHint = focusPane === "search" ? " [Enter] apply [Esc] cancel" : " [/] search [?] help";
|
|
1019
|
+
statusBar.setContent(
|
|
1020
|
+
` ${filtered.length} issue${filtered.length === 1 ? "" : "s"}` + (query ? ` matching "${query}"` : "") + searchHint + filterHint
|
|
1021
|
+
);
|
|
1022
|
+
if (helpVisible) {
|
|
1023
|
+
helpOverlay.show();
|
|
1024
|
+
} else {
|
|
1025
|
+
helpOverlay.hide();
|
|
1026
|
+
}
|
|
1027
|
+
screen.render();
|
|
1028
|
+
}
|
|
1029
|
+
function startKeyboard(keyboard) {
|
|
1030
|
+
screen.on("keypress", (ch, key) => {
|
|
1031
|
+
if (key.full === "C-c" || ch === "") {
|
|
1032
|
+
keyboard.emit("force-quit");
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (lastState && lastState.focusPane === "search") {
|
|
1036
|
+
const name = key.name || "";
|
|
1037
|
+
if (ch === "" || key.full === "C-c") {
|
|
1038
|
+
keyboard.dispatch("", key);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (name === "enter" || name === "return") {
|
|
1042
|
+
keyboard.emit("search-exit");
|
|
1043
|
+
} else if (name === "escape") {
|
|
1044
|
+
keyboard.updateSearch("");
|
|
1045
|
+
keyboard.emit("search-exit");
|
|
1046
|
+
} else if (name === "backspace") {
|
|
1047
|
+
const q = lastState.query || "";
|
|
1048
|
+
keyboard.updateSearch(q.slice(0, -1));
|
|
1049
|
+
} else if (ch && ch.length === 1 && ch >= " ") {
|
|
1050
|
+
keyboard.updateSearch((lastState.query || "") + ch);
|
|
1051
|
+
}
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (lastState && lastState.focusPane === "filter") {
|
|
1055
|
+
const name = key.name || "";
|
|
1056
|
+
if (name === "j" || key.full === "down") {
|
|
1057
|
+
keyboard.emit("filter-scroll-down");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (name === "k" || key.full === "up") {
|
|
1061
|
+
keyboard.emit("filter-scroll-up");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (key.full === "tab") {
|
|
1065
|
+
keyboard.emit("filter-next-section");
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (key.full === "S-tab") {
|
|
1069
|
+
keyboard.emit("filter-prev-section");
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
keyboard.dispatch(key.full || ch || "", key);
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
function destroy() {
|
|
1077
|
+
try {
|
|
1078
|
+
screen.destroy();
|
|
1079
|
+
} catch (_e) {
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return { render, startKeyboard, destroy };
|
|
1083
|
+
}
|
|
1084
|
+
function createNoopRenderer() {
|
|
1085
|
+
return {
|
|
1086
|
+
render(_state) {
|
|
1087
|
+
},
|
|
1088
|
+
startKeyboard(_keyboard) {
|
|
1089
|
+
},
|
|
1090
|
+
destroy() {
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
renderer = { createRenderer };
|
|
1095
|
+
return renderer;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
var search;
|
|
1099
|
+
var hasRequiredSearch;
|
|
1100
|
+
|
|
1101
|
+
function requireSearch () {
|
|
1102
|
+
if (hasRequiredSearch) return search;
|
|
1103
|
+
hasRequiredSearch = 1;
|
|
1104
|
+
class FuzzySearch {
|
|
1105
|
+
/**
|
|
1106
|
+
* @param {Object[]} items - Array of objects to search
|
|
1107
|
+
* @param {Object} [options={}]
|
|
1108
|
+
* @param {string[]} [options.keys=['title']] - Object keys to search against
|
|
1109
|
+
*/
|
|
1110
|
+
constructor(items, options = {}) {
|
|
1111
|
+
this.items = items;
|
|
1112
|
+
this.keys = options.keys || ["title"];
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Search items by query string.
|
|
1116
|
+
*
|
|
1117
|
+
* @param {string} query - Search query (empty string returns all items)
|
|
1118
|
+
* @returns {Object[]} Matching items sorted by score descending
|
|
1119
|
+
*/
|
|
1120
|
+
search(query) {
|
|
1121
|
+
if (!query || typeof query !== "string" || query.trim() === "") {
|
|
1122
|
+
return this.items.slice();
|
|
1123
|
+
}
|
|
1124
|
+
const q = query.toLowerCase().trim();
|
|
1125
|
+
const scored = this.items.map((item) => ({ item, score: this._score(item, q) })).filter(({ score }) => score > 0);
|
|
1126
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1127
|
+
return scored.map(({ item }) => item);
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Score a single item against a query.
|
|
1131
|
+
*
|
|
1132
|
+
* @param {Object} item
|
|
1133
|
+
* @param {string} q - Lowercase trimmed query
|
|
1134
|
+
* @returns {number} Score (0 = no match)
|
|
1135
|
+
* @private
|
|
1136
|
+
*/
|
|
1137
|
+
_score(item, q) {
|
|
1138
|
+
let max = 0;
|
|
1139
|
+
for (const key of this.keys) {
|
|
1140
|
+
const rawVal = item[key];
|
|
1141
|
+
const val = this._normalizeValue(rawVal);
|
|
1142
|
+
if (val === q) return 100;
|
|
1143
|
+
if (val.startsWith(q)) {
|
|
1144
|
+
max = Math.max(max, 80);
|
|
1145
|
+
} else if (val.includes(q)) {
|
|
1146
|
+
max = Math.max(max, 60);
|
|
1147
|
+
} else if (this._fuzzy(val, q)) {
|
|
1148
|
+
max = Math.max(max, 40);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return max;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Normalize a value to a searchable lowercase string.
|
|
1155
|
+
* Arrays (e.g. labels) are joined with space.
|
|
1156
|
+
*
|
|
1157
|
+
* @param {*} val
|
|
1158
|
+
* @returns {string}
|
|
1159
|
+
* @private
|
|
1160
|
+
*/
|
|
1161
|
+
_normalizeValue(val) {
|
|
1162
|
+
if (val === null || val === void 0) return "";
|
|
1163
|
+
if (Array.isArray(val)) {
|
|
1164
|
+
return val.map((v) => typeof v === "object" && v !== null ? v.name || JSON.stringify(v) : String(v)).join(" ").toLowerCase();
|
|
1165
|
+
}
|
|
1166
|
+
return String(val).toLowerCase();
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Check if all characters of `pattern` appear in `str` in order.
|
|
1170
|
+
*
|
|
1171
|
+
* @param {string} str
|
|
1172
|
+
* @param {string} pattern
|
|
1173
|
+
* @returns {boolean}
|
|
1174
|
+
* @private
|
|
1175
|
+
*/
|
|
1176
|
+
_fuzzy(str, pattern) {
|
|
1177
|
+
let pi = 0;
|
|
1178
|
+
for (let si = 0; si < str.length && pi < pattern.length; si++) {
|
|
1179
|
+
if (str[si] === pattern[pi]) pi++;
|
|
1180
|
+
}
|
|
1181
|
+
return pi === pattern.length;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
search = { FuzzySearch };
|
|
1185
|
+
return search;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
var keyboard;
|
|
1189
|
+
var hasRequiredKeyboard;
|
|
1190
|
+
|
|
1191
|
+
function requireKeyboard () {
|
|
1192
|
+
if (hasRequiredKeyboard) return keyboard;
|
|
1193
|
+
hasRequiredKeyboard = 1;
|
|
1194
|
+
const { EventEmitter } = require$$0$2;
|
|
1195
|
+
const DEFAULT_BINDINGS = {
|
|
1196
|
+
// Scroll
|
|
1197
|
+
"j": "scroll-down",
|
|
1198
|
+
"\x1B[B": "scroll-down",
|
|
1199
|
+
// Down arrow
|
|
1200
|
+
"\x1B[1;2B": "scroll-down",
|
|
1201
|
+
// Shift+Down (some terminals)
|
|
1202
|
+
"k": "scroll-up",
|
|
1203
|
+
"\x1B[A": "scroll-up",
|
|
1204
|
+
// Up arrow
|
|
1205
|
+
"\x1B[1;2A": "scroll-up",
|
|
1206
|
+
// Shift+Up
|
|
1207
|
+
// Jump
|
|
1208
|
+
"g": "jump-top",
|
|
1209
|
+
"\x1B[H": "jump-top",
|
|
1210
|
+
// Home
|
|
1211
|
+
"G": "jump-bottom",
|
|
1212
|
+
"\x1B[F": "jump-bottom",
|
|
1213
|
+
// End
|
|
1214
|
+
"\x1B[5~": "page-up",
|
|
1215
|
+
// PgUp
|
|
1216
|
+
"\x1B[6~": "page-down",
|
|
1217
|
+
// PgDn
|
|
1218
|
+
// Select / quit
|
|
1219
|
+
"\r": "select",
|
|
1220
|
+
// Enter
|
|
1221
|
+
"\n": "select",
|
|
1222
|
+
"q": "quit",
|
|
1223
|
+
"\x1B": "quit",
|
|
1224
|
+
// Escape (bare — only if not start of sequence)
|
|
1225
|
+
// Search
|
|
1226
|
+
"/": "search-focus",
|
|
1227
|
+
// Focus
|
|
1228
|
+
" ": "tab-focus",
|
|
1229
|
+
// Tab
|
|
1230
|
+
"\x1B[Z": "tab-focus-reverse",
|
|
1231
|
+
// Shift+Tab
|
|
1232
|
+
// Help
|
|
1233
|
+
"?": "help",
|
|
1234
|
+
// Filter pane
|
|
1235
|
+
"f": "filter-focus",
|
|
1236
|
+
// Open filter pane
|
|
1237
|
+
" ": "filter-toggle",
|
|
1238
|
+
// Toggle item under cursor (Space)
|
|
1239
|
+
"c": "filter-clear",
|
|
1240
|
+
// Clear all filters
|
|
1241
|
+
// Force quit
|
|
1242
|
+
"": "force-quit"
|
|
1243
|
+
// Ctrl+C
|
|
1244
|
+
};
|
|
1245
|
+
class KeyboardHandler extends EventEmitter {
|
|
1246
|
+
/**
|
|
1247
|
+
* @param {Record<string, string>} [bindings=DEFAULT_BINDINGS]
|
|
1248
|
+
* Map of raw key sequences to action names.
|
|
1249
|
+
*/
|
|
1250
|
+
constructor(bindings = DEFAULT_BINDINGS) {
|
|
1251
|
+
super();
|
|
1252
|
+
this.bindings = Object.assign({}, bindings);
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Dispatch a raw key string to the matching action event.
|
|
1256
|
+
*
|
|
1257
|
+
* If no binding is found, emits 'unbound' with the key as argument.
|
|
1258
|
+
*
|
|
1259
|
+
* @param {string} key - Raw keypress string from terminal
|
|
1260
|
+
* @param {Object} [meta={}] - Optional metadata (e.g. { shift, ctrl, name })
|
|
1261
|
+
*/
|
|
1262
|
+
dispatch(key, meta = {}) {
|
|
1263
|
+
const action = this.bindings[key];
|
|
1264
|
+
if (action) {
|
|
1265
|
+
this.emit(action, key, meta);
|
|
1266
|
+
} else {
|
|
1267
|
+
this.emit("unbound", key, meta);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Emit a search-update event for live search input.
|
|
1272
|
+
* Called by the renderer when characters are typed in the search input.
|
|
1273
|
+
*
|
|
1274
|
+
* @param {string} query - Current search string
|
|
1275
|
+
*/
|
|
1276
|
+
updateSearch(query) {
|
|
1277
|
+
this.emit("search-update", query);
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Add or override a key binding.
|
|
1281
|
+
*
|
|
1282
|
+
* @param {string} key - Raw key sequence
|
|
1283
|
+
* @param {string} action - Action name to emit
|
|
1284
|
+
*/
|
|
1285
|
+
bind(key, action) {
|
|
1286
|
+
this.bindings[key] = action;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Remove a key binding.
|
|
1290
|
+
*
|
|
1291
|
+
* @param {string} key - Raw key sequence to unbind
|
|
1292
|
+
*/
|
|
1293
|
+
unbind(key) {
|
|
1294
|
+
delete this.bindings[key];
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
keyboard = { KeyboardHandler, DEFAULT_BINDINGS };
|
|
1298
|
+
return keyboard;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
var filter;
|
|
1302
|
+
var hasRequiredFilter;
|
|
1303
|
+
|
|
1304
|
+
function requireFilter () {
|
|
1305
|
+
if (hasRequiredFilter) return filter;
|
|
1306
|
+
hasRequiredFilter = 1;
|
|
1307
|
+
const STATE_OPTIONS = ["open", "closed", "all"];
|
|
1308
|
+
function extractLabels(issues) {
|
|
1309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1310
|
+
for (const issue of issues) {
|
|
1311
|
+
for (const l of issue.labels || []) {
|
|
1312
|
+
const name = typeof l === "object" ? l.name : String(l);
|
|
1313
|
+
if (name) seen.add(name);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return Array.from(seen).sort();
|
|
1317
|
+
}
|
|
1318
|
+
function extractMilestones(issues) {
|
|
1319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1320
|
+
for (const issue of issues) {
|
|
1321
|
+
if (issue.milestone) {
|
|
1322
|
+
const title = typeof issue.milestone === "object" ? issue.milestone.title : String(issue.milestone);
|
|
1323
|
+
if (title) seen.add(title);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return Array.from(seen).sort();
|
|
1327
|
+
}
|
|
1328
|
+
class FilterState {
|
|
1329
|
+
/**
|
|
1330
|
+
* @param {Object[]} issues - Full unfiltered issue set (used to extract options)
|
|
1331
|
+
* @param {Object} [persisted={}] - Hydrated from .mgw/config.json if available
|
|
1332
|
+
* @param {string[]} [persisted.activeLabels]
|
|
1333
|
+
* @param {string[]} [persisted.activeMilestones]
|
|
1334
|
+
* @param {string} [persisted.activeState]
|
|
1335
|
+
*/
|
|
1336
|
+
constructor(issues, persisted = {}) {
|
|
1337
|
+
this.availableLabels = extractLabels(issues);
|
|
1338
|
+
this.availableMilestones = extractMilestones(issues);
|
|
1339
|
+
const pl = Array.isArray(persisted.activeLabels) ? persisted.activeLabels : [];
|
|
1340
|
+
const pm = Array.isArray(persisted.activeMilestones) ? persisted.activeMilestones : [];
|
|
1341
|
+
const ps = STATE_OPTIONS.includes(persisted.activeState) ? persisted.activeState : "open";
|
|
1342
|
+
this.activeLabels = new Set(pl.filter((l) => this.availableLabels.includes(l)));
|
|
1343
|
+
this.activeMilestones = new Set(pm.filter((m) => this.availableMilestones.includes(m)));
|
|
1344
|
+
this.activeState = ps;
|
|
1345
|
+
this.cursorSection = "labels";
|
|
1346
|
+
this.cursorIndex = 0;
|
|
1347
|
+
}
|
|
1348
|
+
// ── Cursor navigation ──────────────────────────────────────────────────────
|
|
1349
|
+
/**
|
|
1350
|
+
* The ordered list of sections shown in the filter pane.
|
|
1351
|
+
* @returns {string[]}
|
|
1352
|
+
*/
|
|
1353
|
+
get sections() {
|
|
1354
|
+
return ["labels", "milestones", "state"];
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Number of items in the cursor's current section.
|
|
1358
|
+
* @returns {number}
|
|
1359
|
+
*/
|
|
1360
|
+
get currentSectionLength() {
|
|
1361
|
+
switch (this.cursorSection) {
|
|
1362
|
+
case "labels":
|
|
1363
|
+
return Math.max(this.availableLabels.length, 1);
|
|
1364
|
+
case "milestones":
|
|
1365
|
+
return Math.max(this.availableMilestones.length, 1);
|
|
1366
|
+
case "state":
|
|
1367
|
+
return STATE_OPTIONS.length;
|
|
1368
|
+
default:
|
|
1369
|
+
return 1;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Move cursor down within the current section.
|
|
1374
|
+
*/
|
|
1375
|
+
cursorDown() {
|
|
1376
|
+
this.cursorIndex = Math.min(this.cursorIndex + 1, this.currentSectionLength - 1);
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Move cursor up within the current section.
|
|
1380
|
+
*/
|
|
1381
|
+
cursorUp() {
|
|
1382
|
+
this.cursorIndex = Math.max(this.cursorIndex - 1, 0);
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Advance to the next section (wraps around).
|
|
1386
|
+
*/
|
|
1387
|
+
nextSection() {
|
|
1388
|
+
const idx = this.sections.indexOf(this.cursorSection);
|
|
1389
|
+
this.cursorSection = this.sections[(idx + 1) % this.sections.length];
|
|
1390
|
+
this.cursorIndex = 0;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Go to the previous section (wraps around).
|
|
1394
|
+
*/
|
|
1395
|
+
prevSection() {
|
|
1396
|
+
const idx = this.sections.indexOf(this.cursorSection);
|
|
1397
|
+
this.cursorSection = this.sections[(idx - 1 + this.sections.length) % this.sections.length];
|
|
1398
|
+
this.cursorIndex = 0;
|
|
1399
|
+
}
|
|
1400
|
+
// ── Toggle actions ─────────────────────────────────────────────────────────
|
|
1401
|
+
/**
|
|
1402
|
+
* Toggle the item under the cursor.
|
|
1403
|
+
* For labels/milestones: toggle membership in the active set.
|
|
1404
|
+
* For state: cycle to the selected option.
|
|
1405
|
+
*/
|
|
1406
|
+
toggleCursor() {
|
|
1407
|
+
switch (this.cursorSection) {
|
|
1408
|
+
case "labels": {
|
|
1409
|
+
const label = this.availableLabels[this.cursorIndex];
|
|
1410
|
+
if (!label) break;
|
|
1411
|
+
if (this.activeLabels.has(label)) {
|
|
1412
|
+
this.activeLabels.delete(label);
|
|
1413
|
+
} else {
|
|
1414
|
+
this.activeLabels.add(label);
|
|
1415
|
+
}
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
case "milestones": {
|
|
1419
|
+
const ms = this.availableMilestones[this.cursorIndex];
|
|
1420
|
+
if (!ms) break;
|
|
1421
|
+
if (this.activeMilestones.has(ms)) {
|
|
1422
|
+
this.activeMilestones.delete(ms);
|
|
1423
|
+
} else {
|
|
1424
|
+
this.activeMilestones.add(ms);
|
|
1425
|
+
}
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
case "state": {
|
|
1429
|
+
this.activeState = STATE_OPTIONS[this.cursorIndex];
|
|
1430
|
+
break;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Clear all active filters, reset state to 'open'.
|
|
1436
|
+
*/
|
|
1437
|
+
clearAll() {
|
|
1438
|
+
this.activeLabels.clear();
|
|
1439
|
+
this.activeMilestones.clear();
|
|
1440
|
+
this.activeState = "open";
|
|
1441
|
+
}
|
|
1442
|
+
// ── Filtering ──────────────────────────────────────────────────────────────
|
|
1443
|
+
/**
|
|
1444
|
+
* Return true if no filters are active (besides state=open which is the default).
|
|
1445
|
+
* @returns {boolean}
|
|
1446
|
+
*/
|
|
1447
|
+
get isEmpty() {
|
|
1448
|
+
return this.activeLabels.size === 0 && this.activeMilestones.size === 0 && this.activeState === "open";
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Apply the current filter state to an array of issues.
|
|
1452
|
+
*
|
|
1453
|
+
* Label filter: issue must have ALL selected labels.
|
|
1454
|
+
* Milestone filter: issue must match ANY selected milestone.
|
|
1455
|
+
* State filter: applied upstream by the GitHub API; in TUI mode
|
|
1456
|
+
* we apply it client-side against issue.state.
|
|
1457
|
+
*
|
|
1458
|
+
* @param {Object[]} issues - Full unfiltered issue set
|
|
1459
|
+
* @returns {Object[]} Filtered subset
|
|
1460
|
+
*/
|
|
1461
|
+
apply(issues) {
|
|
1462
|
+
return issues.filter((issue) => {
|
|
1463
|
+
if (this.activeState !== "all") {
|
|
1464
|
+
const issueState = (issue.state || "open").toLowerCase();
|
|
1465
|
+
if (issueState !== this.activeState) return false;
|
|
1466
|
+
}
|
|
1467
|
+
if (this.activeLabels.size > 0) {
|
|
1468
|
+
const issueLabels = new Set(
|
|
1469
|
+
(issue.labels || []).map((l) => typeof l === "object" ? l.name : String(l))
|
|
1470
|
+
);
|
|
1471
|
+
for (const required of this.activeLabels) {
|
|
1472
|
+
if (!issueLabels.has(required)) return false;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (this.activeMilestones.size > 0) {
|
|
1476
|
+
const ms = issue.milestone ? typeof issue.milestone === "object" ? issue.milestone.title : String(issue.milestone) : null;
|
|
1477
|
+
if (!ms || !this.activeMilestones.has(ms)) return false;
|
|
1478
|
+
}
|
|
1479
|
+
return true;
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
// ── Serialisation ──────────────────────────────────────────────────────────
|
|
1483
|
+
/**
|
|
1484
|
+
* Return a plain object suitable for JSON persistence in .mgw/config.json.
|
|
1485
|
+
*
|
|
1486
|
+
* @returns {{activeLabels: string[], activeMilestones: string[], activeState: string}}
|
|
1487
|
+
*/
|
|
1488
|
+
toJSON() {
|
|
1489
|
+
return {
|
|
1490
|
+
activeLabels: Array.from(this.activeLabels),
|
|
1491
|
+
activeMilestones: Array.from(this.activeMilestones),
|
|
1492
|
+
activeState: this.activeState
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
filter = { FilterState, extractLabels, extractMilestones, STATE_OPTIONS };
|
|
1497
|
+
return filter;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
var graceful;
|
|
1501
|
+
var hasRequiredGraceful;
|
|
1502
|
+
|
|
1503
|
+
function requireGraceful () {
|
|
1504
|
+
if (hasRequiredGraceful) return graceful;
|
|
1505
|
+
hasRequiredGraceful = 1;
|
|
1506
|
+
function isInteractive() {
|
|
1507
|
+
if (process.env.MGW_NO_TUI === "1") return false;
|
|
1508
|
+
if (process.env.CI) return false;
|
|
1509
|
+
return Boolean(process.stdout.isTTY);
|
|
1510
|
+
}
|
|
1511
|
+
function renderStaticTable(issues) {
|
|
1512
|
+
if (!issues || issues.length === 0) {
|
|
1513
|
+
console.log("No issues found.");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const COL = { num: 6, title: 40, labels: 25, age: 5 };
|
|
1517
|
+
const totalWidth = COL.num + 1 + COL.title + 1 + COL.labels + 1 + COL.age;
|
|
1518
|
+
const header = [
|
|
1519
|
+
"#".padEnd(COL.num),
|
|
1520
|
+
"Title".padEnd(COL.title),
|
|
1521
|
+
"Labels".padEnd(COL.labels),
|
|
1522
|
+
"Age".padEnd(COL.age)
|
|
1523
|
+
].join(" ");
|
|
1524
|
+
const separator = "-".repeat(totalWidth);
|
|
1525
|
+
console.log(header);
|
|
1526
|
+
console.log(separator);
|
|
1527
|
+
for (const issue of issues) {
|
|
1528
|
+
const num = String(issue.number || "").padEnd(COL.num);
|
|
1529
|
+
const title = _truncate(issue.title || "", COL.title).padEnd(COL.title);
|
|
1530
|
+
const labelStr = _formatLabels(issue.labels).slice(0, COL.labels).padEnd(COL.labels);
|
|
1531
|
+
const age = _relativeAge(issue.createdAt).padEnd(COL.age);
|
|
1532
|
+
console.log([num, title, labelStr, age].join(" "));
|
|
1533
|
+
}
|
|
1534
|
+
console.log(separator);
|
|
1535
|
+
console.log(`${issues.length} issue${issues.length === 1 ? "" : "s"}`);
|
|
1536
|
+
}
|
|
1537
|
+
function _formatLabels(labels) {
|
|
1538
|
+
if (!Array.isArray(labels) || labels.length === 0) return "";
|
|
1539
|
+
return labels.map((l) => typeof l === "object" && l !== null ? l.name : String(l)).join(", ");
|
|
1540
|
+
}
|
|
1541
|
+
function _truncate(str, maxLen) {
|
|
1542
|
+
if (str.length <= maxLen) return str;
|
|
1543
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
1544
|
+
}
|
|
1545
|
+
function _relativeAge(dateStr) {
|
|
1546
|
+
if (!dateStr) return "-";
|
|
1547
|
+
let ms;
|
|
1548
|
+
try {
|
|
1549
|
+
ms = Date.now() - new Date(dateStr).getTime();
|
|
1550
|
+
} catch (e) {
|
|
1551
|
+
return "-";
|
|
1552
|
+
}
|
|
1553
|
+
if (ms < 0) return "now";
|
|
1554
|
+
const minutes = Math.floor(ms / 6e4);
|
|
1555
|
+
const hours = Math.floor(ms / 36e5);
|
|
1556
|
+
const days = Math.floor(ms / 864e5);
|
|
1557
|
+
if (days === 0) return hours > 0 ? `${hours}h` : minutes > 0 ? `${minutes}m` : "now";
|
|
1558
|
+
if (days === 1) return "1d";
|
|
1559
|
+
if (days < 7) return `${days}d`;
|
|
1560
|
+
if (days < 30) return `${Math.floor(days / 7)}w`;
|
|
1561
|
+
if (days < 365) return `${Math.floor(days / 30)}mo`;
|
|
1562
|
+
return `${Math.floor(days / 365)}yr`;
|
|
1563
|
+
}
|
|
1564
|
+
graceful = { isInteractive, renderStaticTable };
|
|
1565
|
+
return graceful;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
var tui;
|
|
1569
|
+
var hasRequiredTui;
|
|
1570
|
+
|
|
1571
|
+
function requireTui () {
|
|
1572
|
+
if (hasRequiredTui) return tui;
|
|
1573
|
+
hasRequiredTui = 1;
|
|
1574
|
+
const { createRenderer } = requireRenderer();
|
|
1575
|
+
const { FuzzySearch } = requireSearch();
|
|
1576
|
+
const { KeyboardHandler } = requireKeyboard();
|
|
1577
|
+
const { FilterState } = requireFilter();
|
|
1578
|
+
const { isInteractive, renderStaticTable } = requireGraceful();
|
|
1579
|
+
function _loadPersistedFilters() {
|
|
1580
|
+
try {
|
|
1581
|
+
const path = require("path");
|
|
1582
|
+
const fs = require("fs");
|
|
1583
|
+
const configPath = path.join(process.cwd(), ".mgw", "config.json");
|
|
1584
|
+
if (!fs.existsSync(configPath)) return {};
|
|
1585
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
1586
|
+
const data = JSON.parse(raw);
|
|
1587
|
+
return data.tuiFilters || {};
|
|
1588
|
+
} catch (_e) {
|
|
1589
|
+
return {};
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function _savePersistedFilters(filterState) {
|
|
1593
|
+
try {
|
|
1594
|
+
const path = require("path");
|
|
1595
|
+
const fs = require("fs");
|
|
1596
|
+
const configPath = path.join(process.cwd(), ".mgw", "config.json");
|
|
1597
|
+
let data = {};
|
|
1598
|
+
if (fs.existsSync(configPath)) {
|
|
1599
|
+
try {
|
|
1600
|
+
data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1601
|
+
} catch (_e) {
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
data.tuiFilters = filterState.toJSON();
|
|
1605
|
+
fs.writeFileSync(configPath, JSON.stringify(data, null, 2));
|
|
1606
|
+
} catch (_e) {
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async function createIssuesBrowser(options) {
|
|
1610
|
+
const {
|
|
1611
|
+
issues = [],
|
|
1612
|
+
onSelect,
|
|
1613
|
+
onQuit,
|
|
1614
|
+
initialQuery = "",
|
|
1615
|
+
initialFilter = {}
|
|
1616
|
+
} = options;
|
|
1617
|
+
if (!isInteractive()) {
|
|
1618
|
+
const displayed = initialQuery ? new FuzzySearch(issues, { keys: ["title", "number", "labels"] }).search(initialQuery) : issues;
|
|
1619
|
+
renderStaticTable(displayed);
|
|
1620
|
+
if (typeof onQuit === "function") onQuit();
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const persisted = _loadPersistedFilters();
|
|
1624
|
+
const seedFilters = Object.assign({}, persisted);
|
|
1625
|
+
if (initialFilter.label) seedFilters.activeLabels = [initialFilter.label];
|
|
1626
|
+
if (initialFilter.milestone) seedFilters.activeMilestones = [initialFilter.milestone];
|
|
1627
|
+
if (initialFilter.state) seedFilters.activeState = initialFilter.state;
|
|
1628
|
+
const filterState = new FilterState(issues, seedFilters);
|
|
1629
|
+
new FuzzySearch(issues, { keys: ["title", "number", "labels"] });
|
|
1630
|
+
const renderer = createRenderer();
|
|
1631
|
+
const keyboard = new KeyboardHandler();
|
|
1632
|
+
const PAGE_SIZE = 10;
|
|
1633
|
+
let query = initialQuery;
|
|
1634
|
+
let selectedIndex = 0;
|
|
1635
|
+
let focusPane = "list";
|
|
1636
|
+
let running = true;
|
|
1637
|
+
let helpVisible = false;
|
|
1638
|
+
function _searchIn(subset, q) {
|
|
1639
|
+
if (!q) return subset;
|
|
1640
|
+
return new FuzzySearch(subset, { keys: ["title", "number", "labels"] }).search(q);
|
|
1641
|
+
}
|
|
1642
|
+
let filtered = _searchIn(filterState.apply(issues), query);
|
|
1643
|
+
function draw() {
|
|
1644
|
+
return renderer.render({
|
|
1645
|
+
filtered,
|
|
1646
|
+
selectedIndex,
|
|
1647
|
+
query,
|
|
1648
|
+
focusPane,
|
|
1649
|
+
filterState,
|
|
1650
|
+
helpVisible
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
function clampIndex() {
|
|
1654
|
+
if (selectedIndex < 0) selectedIndex = 0;
|
|
1655
|
+
if (filtered.length > 0 && selectedIndex >= filtered.length) {
|
|
1656
|
+
selectedIndex = filtered.length - 1;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
function refilter() {
|
|
1660
|
+
filtered = _searchIn(filterState.apply(issues), query);
|
|
1661
|
+
selectedIndex = 0;
|
|
1662
|
+
}
|
|
1663
|
+
keyboard.on("scroll-down", () => {
|
|
1664
|
+
if (focusPane === "filter") return;
|
|
1665
|
+
selectedIndex++;
|
|
1666
|
+
clampIndex();
|
|
1667
|
+
draw();
|
|
1668
|
+
});
|
|
1669
|
+
keyboard.on("scroll-up", () => {
|
|
1670
|
+
if (focusPane === "filter") return;
|
|
1671
|
+
selectedIndex--;
|
|
1672
|
+
clampIndex();
|
|
1673
|
+
draw();
|
|
1674
|
+
});
|
|
1675
|
+
keyboard.on("jump-top", () => {
|
|
1676
|
+
selectedIndex = 0;
|
|
1677
|
+
draw();
|
|
1678
|
+
});
|
|
1679
|
+
keyboard.on("jump-bottom", () => {
|
|
1680
|
+
selectedIndex = Math.max(0, filtered.length - 1);
|
|
1681
|
+
draw();
|
|
1682
|
+
});
|
|
1683
|
+
keyboard.on("page-down", () => {
|
|
1684
|
+
selectedIndex = Math.min(selectedIndex + PAGE_SIZE, Math.max(0, filtered.length - 1));
|
|
1685
|
+
draw();
|
|
1686
|
+
});
|
|
1687
|
+
keyboard.on("page-up", () => {
|
|
1688
|
+
selectedIndex = Math.max(selectedIndex - PAGE_SIZE, 0);
|
|
1689
|
+
draw();
|
|
1690
|
+
});
|
|
1691
|
+
keyboard.on("select", () => {
|
|
1692
|
+
if (focusPane === "filter") {
|
|
1693
|
+
_savePersistedFilters(filterState);
|
|
1694
|
+
focusPane = "list";
|
|
1695
|
+
draw();
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (filtered.length > 0 && typeof onSelect === "function") {
|
|
1699
|
+
const issue = filtered[selectedIndex];
|
|
1700
|
+
running = false;
|
|
1701
|
+
renderer.destroy();
|
|
1702
|
+
onSelect(issue);
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
keyboard.on("quit", () => {
|
|
1706
|
+
if (helpVisible) {
|
|
1707
|
+
helpVisible = false;
|
|
1708
|
+
draw();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (focusPane === "filter") {
|
|
1712
|
+
_savePersistedFilters(filterState);
|
|
1713
|
+
focusPane = "list";
|
|
1714
|
+
draw();
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (focusPane === "search") {
|
|
1718
|
+
focusPane = "list";
|
|
1719
|
+
draw();
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (running) {
|
|
1723
|
+
running = false;
|
|
1724
|
+
renderer.destroy();
|
|
1725
|
+
if (typeof onQuit === "function") onQuit();
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
keyboard.on("force-quit", () => {
|
|
1729
|
+
if (running) {
|
|
1730
|
+
running = false;
|
|
1731
|
+
renderer.destroy();
|
|
1732
|
+
process.exit(0);
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
keyboard.on("tab-focus", () => {
|
|
1736
|
+
const order = ["list", "detail", "filter"];
|
|
1737
|
+
const current = order.indexOf(focusPane);
|
|
1738
|
+
focusPane = order[(current + 1) % order.length];
|
|
1739
|
+
draw();
|
|
1740
|
+
});
|
|
1741
|
+
keyboard.on("tab-focus-reverse", () => {
|
|
1742
|
+
const order = ["list", "detail", "filter"];
|
|
1743
|
+
const current = order.indexOf(focusPane);
|
|
1744
|
+
const base = current === -1 ? 0 : current;
|
|
1745
|
+
focusPane = order[(base - 1 + order.length) % order.length];
|
|
1746
|
+
draw();
|
|
1747
|
+
});
|
|
1748
|
+
keyboard.on("search-focus", () => {
|
|
1749
|
+
focusPane = "search";
|
|
1750
|
+
draw();
|
|
1751
|
+
});
|
|
1752
|
+
keyboard.on("search-exit", () => {
|
|
1753
|
+
focusPane = "list";
|
|
1754
|
+
draw();
|
|
1755
|
+
});
|
|
1756
|
+
keyboard.on("search-update", (newQuery) => {
|
|
1757
|
+
query = newQuery;
|
|
1758
|
+
refilter();
|
|
1759
|
+
draw();
|
|
1760
|
+
});
|
|
1761
|
+
keyboard.on("help", () => {
|
|
1762
|
+
helpVisible = !helpVisible;
|
|
1763
|
+
draw();
|
|
1764
|
+
});
|
|
1765
|
+
keyboard.on("filter-focus", () => {
|
|
1766
|
+
if (focusPane === "filter") {
|
|
1767
|
+
_savePersistedFilters(filterState);
|
|
1768
|
+
focusPane = "list";
|
|
1769
|
+
} else {
|
|
1770
|
+
focusPane = "filter";
|
|
1771
|
+
}
|
|
1772
|
+
draw();
|
|
1773
|
+
});
|
|
1774
|
+
keyboard.on("filter-scroll-down", () => {
|
|
1775
|
+
filterState.cursorDown();
|
|
1776
|
+
draw();
|
|
1777
|
+
});
|
|
1778
|
+
keyboard.on("filter-scroll-up", () => {
|
|
1779
|
+
filterState.cursorUp();
|
|
1780
|
+
draw();
|
|
1781
|
+
});
|
|
1782
|
+
keyboard.on("filter-next-section", () => {
|
|
1783
|
+
filterState.nextSection();
|
|
1784
|
+
draw();
|
|
1785
|
+
});
|
|
1786
|
+
keyboard.on("filter-prev-section", () => {
|
|
1787
|
+
filterState.prevSection();
|
|
1788
|
+
draw();
|
|
1789
|
+
});
|
|
1790
|
+
keyboard.on("filter-toggle", () => {
|
|
1791
|
+
if (focusPane !== "filter") return;
|
|
1792
|
+
filterState.toggleCursor();
|
|
1793
|
+
refilter();
|
|
1794
|
+
_savePersistedFilters(filterState);
|
|
1795
|
+
draw();
|
|
1796
|
+
});
|
|
1797
|
+
keyboard.on("filter-clear", () => {
|
|
1798
|
+
filterState.clearAll();
|
|
1799
|
+
refilter();
|
|
1800
|
+
_savePersistedFilters(filterState);
|
|
1801
|
+
draw();
|
|
1802
|
+
});
|
|
1803
|
+
await draw();
|
|
1804
|
+
if (typeof renderer.startKeyboard === "function") {
|
|
1805
|
+
renderer.startKeyboard(keyboard);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
tui = { createIssuesBrowser };
|
|
1809
|
+
return tui;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
exports.getDefaultExportFromCjs = getDefaultExportFromCjs;
|
|
1813
|
+
exports.requireClaude = requireClaude;
|
|
1814
|
+
exports.requireGithub = requireGithub;
|
|
1815
|
+
exports.requireOutput = requireOutput;
|
|
1816
|
+
exports.requireSpinner = requireSpinner;
|
|
1817
|
+
exports.requireState = requireState;
|
|
1818
|
+
exports.requireTui = requireTui;
|