@kud/gh-cockpit 0.0.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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/chunk-A3NXVFEI.js +311 -0
- package/dist/extensions/index.d.ts +7 -0
- package/dist/extensions/index.js +69 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +624 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import { __export, withRetry, toGHItem, signalPath, computeHealth, buildInboxQuery, lib_exports, AiLauncher, seedPromptFor, CopyPromptNotice, DrillView, __reExport } from './chunk-A3NXVFEI.js';
|
|
2
|
+
export { buildInboxQuery, computeHealth, signalPath, toGHItem, withRetry } from './chunk-A3NXVFEI.js';
|
|
3
|
+
import { parsePatterns, HealthPanel, CommentsPanel, inboxConfig } from '@kud/gh-ink';
|
|
4
|
+
import { $ } from 'zx';
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
import { useInput, Box, Text } from 'ink';
|
|
7
|
+
import { useTabs, Tabs, useListCursor, colors, ScrollView } from '@kud/ink-ui';
|
|
8
|
+
import { fetchHealth, fetchPrComments } from '@kud/gh';
|
|
9
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
10
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
// src/index.ts
|
|
15
|
+
var index_exports = {};
|
|
16
|
+
__export(index_exports, {
|
|
17
|
+
IssueView: () => IssueView,
|
|
18
|
+
PrView: () => PrView,
|
|
19
|
+
buildInboxQuery: () => buildInboxQuery,
|
|
20
|
+
checkDrillFor: () => checkDrillFor,
|
|
21
|
+
computeHealth: () => computeHealth,
|
|
22
|
+
defineCockpit: () => defineCockpit,
|
|
23
|
+
detailFor: () => detailFor,
|
|
24
|
+
hasPatterns: () => hasPatterns,
|
|
25
|
+
parseArgs: () => parseArgs,
|
|
26
|
+
registerCheckDrills: () => registerCheckDrills,
|
|
27
|
+
signalPath: () => signalPath,
|
|
28
|
+
toGHItem: () => toGHItem,
|
|
29
|
+
withRetry: () => withRetry
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// src/config.ts
|
|
33
|
+
var defineCockpit = (config) => config;
|
|
34
|
+
var parseArgs = (argv) => {
|
|
35
|
+
const include = [];
|
|
36
|
+
const exclude = [];
|
|
37
|
+
let here = false;
|
|
38
|
+
let named;
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const arg = argv[i];
|
|
41
|
+
if (arg === "--here") {
|
|
42
|
+
here = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const eq = arg.match(/^--(include|exclude)=(.*)$/);
|
|
46
|
+
if (eq) {
|
|
47
|
+
(eq[1] === "include" ? include : exclude).push(...parsePatterns(eq[2]));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg === "--include" || arg === "--exclude") {
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
if (next === void 0 || next.startsWith("-")) continue;
|
|
53
|
+
i++;
|
|
54
|
+
(arg === "--include" ? include : exclude).push(...parsePatterns(next));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!arg.startsWith("-") && !named) named = arg;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
here,
|
|
61
|
+
named,
|
|
62
|
+
filter: { include, exclude }
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
var hasPatterns = (f) => (f.include?.length ?? 0) > 0 || (f.exclude?.length ?? 0) > 0;
|
|
66
|
+
|
|
67
|
+
// src/views/check-drill.tsx
|
|
68
|
+
var registered = [];
|
|
69
|
+
var registerCheckDrills = (drills) => {
|
|
70
|
+
registered = drills;
|
|
71
|
+
};
|
|
72
|
+
var checkDrillFor = (url) => registered.find((d) => d.match(url)) ?? null;
|
|
73
|
+
var fetchComments = async (repo, number, kind = "pr") => {
|
|
74
|
+
if (kind === "issue") return fetchIssueConversation(repo, number);
|
|
75
|
+
const [owner, name] = repo.split("/");
|
|
76
|
+
return fetchPrComments(owner, name, number);
|
|
77
|
+
};
|
|
78
|
+
var ISSUE_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
79
|
+
repository(owner: $owner, name: $name) {
|
|
80
|
+
issue(number: $number) {
|
|
81
|
+
comments(first: 50) { nodes { author { login } body createdAt url } }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}`;
|
|
85
|
+
var fetchIssueConversation = async (repo, number) => {
|
|
86
|
+
const [owner, name] = repo.split("/");
|
|
87
|
+
const [body, rest] = await Promise.all([
|
|
88
|
+
fetchBody("issue", repo, number),
|
|
89
|
+
$`gh api graphql -f query=${ISSUE_QUERY} -f owner=${owner} -f name=${name} -F number=${number}`.quiet()
|
|
90
|
+
]);
|
|
91
|
+
const nodes = JSON.parse(rest.stdout).data.repository.issue.comments.nodes;
|
|
92
|
+
const comments = nodes.map((c) => ({
|
|
93
|
+
author: c.author?.login ?? "ghost",
|
|
94
|
+
body: (c.body ?? "").trim(),
|
|
95
|
+
createdAt: c.createdAt,
|
|
96
|
+
url: c.url
|
|
97
|
+
}));
|
|
98
|
+
return {
|
|
99
|
+
headRef: "",
|
|
100
|
+
conversation: body ? [body, ...comments] : comments,
|
|
101
|
+
threads: []
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
var fetchBody = async (kind, repo, number) => {
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await $`gh ${kind} view ${number} --repo ${repo} --json body,author,createdAt`.quiet();
|
|
107
|
+
const node = JSON.parse(stdout);
|
|
108
|
+
const text = (node.body ?? "").trim();
|
|
109
|
+
return text ? {
|
|
110
|
+
author: node.author?.login ?? "",
|
|
111
|
+
body: text,
|
|
112
|
+
createdAt: node.createdAt
|
|
113
|
+
} : null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var jobIdOf = (url) => url?.match(/\/job\/(\d+)/)?.[1] ?? null;
|
|
119
|
+
var stripTimestamp = (line) => line.replace(/^\d{4}-\d\d-\d\dT[\d:.]+Z\s?/, "").replace(/\s+$/, "");
|
|
120
|
+
var colorFor = (clean) => {
|
|
121
|
+
const lower = clean.toLowerCase();
|
|
122
|
+
if (/(^|\W)(error|fail(ed|ure)?)\b/.test(lower)) return colors.error;
|
|
123
|
+
if (/(^|\W)warn(ing)?\b/.test(lower)) return colors.warning;
|
|
124
|
+
return void 0;
|
|
125
|
+
};
|
|
126
|
+
var processLog = (raw) => {
|
|
127
|
+
const lines = [];
|
|
128
|
+
let firstError = -1;
|
|
129
|
+
for (const l of raw) {
|
|
130
|
+
const clean = stripTimestamp(l);
|
|
131
|
+
const cmd = clean.match(/^##\[(\w+)\](.*)$/);
|
|
132
|
+
if (cmd) {
|
|
133
|
+
const [, kind, rest] = cmd;
|
|
134
|
+
if (kind === "endgroup") continue;
|
|
135
|
+
if (kind === "group") {
|
|
136
|
+
lines.push({ text: "\u25B8 " + rest, dim: true, bold: true });
|
|
137
|
+
} else if (kind === "error") {
|
|
138
|
+
if (firstError < 0) firstError = lines.length;
|
|
139
|
+
lines.push({ text: rest, color: colors.error });
|
|
140
|
+
} else if (kind === "warning") {
|
|
141
|
+
lines.push({ text: rest, color: colors.warning });
|
|
142
|
+
} else {
|
|
143
|
+
lines.push({ text: rest, dim: true });
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
lines.push({ text: clean, color: colorFor(clean) });
|
|
148
|
+
}
|
|
149
|
+
const jumpTo = firstError >= 0 ? Math.max(0, firstError - 3) : Math.max(0, lines.length - 1);
|
|
150
|
+
return { lines, jumpTo };
|
|
151
|
+
};
|
|
152
|
+
var CheckLogView = ({
|
|
153
|
+
repo,
|
|
154
|
+
jobId,
|
|
155
|
+
name,
|
|
156
|
+
url,
|
|
157
|
+
onBack
|
|
158
|
+
}) => {
|
|
159
|
+
const [state, setState] = useState({ phase: "loading" });
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
let live = true;
|
|
162
|
+
$`gh api repos/${repo}/actions/jobs/${jobId}/logs`.quiet().then(
|
|
163
|
+
(r) => live && setState({ phase: "ready", lines: r.stdout.split("\n") })
|
|
164
|
+
).catch(
|
|
165
|
+
(e) => live && setState({ phase: "error", message: e.message })
|
|
166
|
+
);
|
|
167
|
+
return () => {
|
|
168
|
+
live = false;
|
|
169
|
+
};
|
|
170
|
+
}, [repo, jobId]);
|
|
171
|
+
useInput((input, key) => {
|
|
172
|
+
if (key.escape || input === "q") return onBack();
|
|
173
|
+
if (input === "o" && url) $`open ${url}`.catch(() => {
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
let lines = [];
|
|
177
|
+
let jumpTo = 0;
|
|
178
|
+
if (state.phase === "loading")
|
|
179
|
+
lines = [{ text: "Fetching log\u2026", color: colors.info }];
|
|
180
|
+
else if (state.phase === "error")
|
|
181
|
+
lines = [{ text: `Error: ${state.message}`, color: colors.error }];
|
|
182
|
+
else ({ lines, jumpTo } = processLog(state.lines));
|
|
183
|
+
return /* @__PURE__ */ jsx(
|
|
184
|
+
DrillView,
|
|
185
|
+
{
|
|
186
|
+
title: `${name} \u2014 log`,
|
|
187
|
+
subtitle: state.phase === "ready" ? `${state.lines.length} lines` : void 0,
|
|
188
|
+
hints: [
|
|
189
|
+
["\u2191\u2193/space", "scroll"],
|
|
190
|
+
["g/G", "top/tail"],
|
|
191
|
+
["o", "open"],
|
|
192
|
+
["q/esc", "back"]
|
|
193
|
+
],
|
|
194
|
+
children: /* @__PURE__ */ jsx(ScrollView, { lines, initialStart: jumpTo })
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
var QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
199
|
+
repository(owner: $owner, name: $name) {
|
|
200
|
+
pullRequest(number: $number) {
|
|
201
|
+
reviewThreads(first: 100) { nodes { path line } }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}`;
|
|
205
|
+
var fetchFiles = async (repo, number) => {
|
|
206
|
+
const [owner, name] = repo.split("/");
|
|
207
|
+
const out = await $`gh api graphql -f query=${QUERY} -f owner=${owner} -f name=${name} -F number=${number}`.quiet();
|
|
208
|
+
const nodes = JSON.parse(out.stdout).data.repository.pullRequest.reviewThreads.nodes ?? [];
|
|
209
|
+
const seen = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const n of nodes)
|
|
211
|
+
if (n.path && !seen.has(n.path))
|
|
212
|
+
seen.set(n.path, { path: n.path, line: n.line });
|
|
213
|
+
return [...seen.values()];
|
|
214
|
+
};
|
|
215
|
+
var openInEditor = async (repoPath, file) => {
|
|
216
|
+
const target = file.line ? `${file.path}:${file.line}` : file.path;
|
|
217
|
+
for (const ed of ["cursor", "code"]) {
|
|
218
|
+
if ((await $({ nothrow: true, quiet: true })`command -v ${ed}`).exitCode === 0) {
|
|
219
|
+
void $({
|
|
220
|
+
nothrow: true,
|
|
221
|
+
quiet: true
|
|
222
|
+
})`${ed} -g ${repoPath}/${target}`.catch(() => {
|
|
223
|
+
});
|
|
224
|
+
return ed;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const editor = process.env.EDITOR || "vi";
|
|
228
|
+
await (0, lib_exports.openInTab)(
|
|
229
|
+
`cd ${repoPath} && ${editor} ${file.line ? `+${file.line} ` : ""}${file.path}`
|
|
230
|
+
);
|
|
231
|
+
return editor;
|
|
232
|
+
};
|
|
233
|
+
var FilePicker = ({
|
|
234
|
+
repo,
|
|
235
|
+
number,
|
|
236
|
+
onBack
|
|
237
|
+
}) => {
|
|
238
|
+
const [files, setFiles] = useState(null);
|
|
239
|
+
const [note, setNote] = useState(null);
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
let live = true;
|
|
242
|
+
fetchFiles(repo, number).then((f) => live && setFiles(f)).catch((e) => live && setNote(e.message));
|
|
243
|
+
return () => {
|
|
244
|
+
live = false;
|
|
245
|
+
};
|
|
246
|
+
}, [repo, number]);
|
|
247
|
+
const list = files ?? [];
|
|
248
|
+
const { cursor } = useListCursor(list.length);
|
|
249
|
+
const safeCursor = Math.min(cursor, Math.max(0, list.length - 1));
|
|
250
|
+
const open = async (f) => {
|
|
251
|
+
setNote(`\u22EF resolving ${repo}\u2026`);
|
|
252
|
+
const repoPath = await (0, lib_exports.resolveRepoPath)(repo);
|
|
253
|
+
if (!repoPath) {
|
|
254
|
+
setNote(`\u2717 ${repo} isn't checked out locally`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const ed = await openInEditor(repoPath, f);
|
|
258
|
+
setNote(`\u2197 ${f.path}${f.line ? `:${f.line}` : ""} \u2014 ${ed}`);
|
|
259
|
+
};
|
|
260
|
+
useInput((input, key) => {
|
|
261
|
+
if (key.escape || input === "q") return onBack();
|
|
262
|
+
if (key.return && list[safeCursor]) void open(list[safeCursor]);
|
|
263
|
+
});
|
|
264
|
+
return /* @__PURE__ */ jsxs(
|
|
265
|
+
DrillView,
|
|
266
|
+
{
|
|
267
|
+
title: `Files \xB7 #${number} \xB7 ${repo}`,
|
|
268
|
+
subtitle: "files touched by review comments",
|
|
269
|
+
hints: [
|
|
270
|
+
["\u2191\u2193", "nav"],
|
|
271
|
+
["\u21B5", "open in editor"],
|
|
272
|
+
["q/esc", "back"]
|
|
273
|
+
],
|
|
274
|
+
children: [
|
|
275
|
+
!files ? /* @__PURE__ */ jsx(Text, { color: colors.info, children: "Fetching files\u2026" }) : list.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No files referenced in review comments." }) : /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: list.map((f, i) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
276
|
+
/* @__PURE__ */ jsx(Text, { color: colors.info, children: i === safeCursor ? " \u276F " : " " }),
|
|
277
|
+
/* @__PURE__ */ jsx(Text, { bold: i === safeCursor, children: f.path }),
|
|
278
|
+
f.line ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: `:${f.line}` }) : null
|
|
279
|
+
] }, f.path)) }),
|
|
280
|
+
note ? /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.success, children: " " + note }) }) : null
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
var cacheDir = () => join(
|
|
286
|
+
process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
|
|
287
|
+
inboxConfig().cacheNamespace
|
|
288
|
+
);
|
|
289
|
+
var cacheFile = (key) => join(cacheDir(), `${key.replace(/[^a-z0-9._-]/gi, "-")}.json`);
|
|
290
|
+
var readJson = (key) => {
|
|
291
|
+
try {
|
|
292
|
+
return JSON.parse(readFileSync(cacheFile(key), "utf8")).v;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var writeJson = (key, v) => {
|
|
298
|
+
try {
|
|
299
|
+
mkdirSync(cacheDir(), { recursive: true });
|
|
300
|
+
writeFileSync(cacheFile(key), JSON.stringify({ v, at: Date.now() }));
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var useCachedResource = (key, fetcher) => {
|
|
305
|
+
const [data, setData] = useState(() => readJson(key));
|
|
306
|
+
const [error, setError] = useState(null);
|
|
307
|
+
const [tick, setTick] = useState(0);
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
let live = true;
|
|
310
|
+
fetcher().then((fresh) => {
|
|
311
|
+
if (!live) return;
|
|
312
|
+
writeJson(key, fresh);
|
|
313
|
+
setData(fresh);
|
|
314
|
+
setError(null);
|
|
315
|
+
}).catch((e) => live && setError(e.message));
|
|
316
|
+
return () => {
|
|
317
|
+
live = false;
|
|
318
|
+
};
|
|
319
|
+
}, [key, tick]);
|
|
320
|
+
return {
|
|
321
|
+
data,
|
|
322
|
+
error,
|
|
323
|
+
loading: data === null && error === null,
|
|
324
|
+
reload: () => setTick((t) => t + 1)
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
var PrView = ({
|
|
328
|
+
item,
|
|
329
|
+
login,
|
|
330
|
+
onBack,
|
|
331
|
+
defaultTab,
|
|
332
|
+
onRefresh,
|
|
333
|
+
onRemove,
|
|
334
|
+
onMerged
|
|
335
|
+
}) => {
|
|
336
|
+
const [log, setLog] = useState(null);
|
|
337
|
+
const [ai, setAi] = useState(false);
|
|
338
|
+
const [files, setFiles] = useState(false);
|
|
339
|
+
const [copy, setCopy] = useState(false);
|
|
340
|
+
const [replying, setReplying] = useState(false);
|
|
341
|
+
const menu = (0, lib_exports.useActionMenu)();
|
|
342
|
+
const comments = useCachedResource(
|
|
343
|
+
`pr-comments-${item.repo}-${item.number}`,
|
|
344
|
+
() => fetchComments(item.repo, item.number, "pr")
|
|
345
|
+
);
|
|
346
|
+
const unresolvedCount = (comments.data?.threads ?? []).filter(
|
|
347
|
+
(t) => !t.isResolved
|
|
348
|
+
).length;
|
|
349
|
+
const conversationLabel = unresolvedCount ? `Conversation (${unresolvedCount} unresolved)` : "Conversation";
|
|
350
|
+
const tabItems = [
|
|
351
|
+
{ value: "health", label: "Health" },
|
|
352
|
+
{ value: "conversation", label: conversationLabel }
|
|
353
|
+
];
|
|
354
|
+
const inputActive = log === null && !ai && !files && !replying && !copy;
|
|
355
|
+
const menuOpen = menu.actions !== null;
|
|
356
|
+
const { active, setActive } = useTabs(tabItems, {
|
|
357
|
+
initial: defaultTab ?? "health",
|
|
358
|
+
isActive: inputActive && !menuOpen
|
|
359
|
+
});
|
|
360
|
+
const tab = active ?? "health";
|
|
361
|
+
const health = useCachedResource(
|
|
362
|
+
`pr-health-${item.repo}-${item.number}`,
|
|
363
|
+
() => fetchHealth(item.repo, item.number)
|
|
364
|
+
);
|
|
365
|
+
const checkLabel = (c) => c.workflowName ? `${c.workflowName} / ${c.context ?? c.name}` : c.context ?? c.name ?? "";
|
|
366
|
+
const onOpenCheck = (c) => {
|
|
367
|
+
const url = c.detailsUrl ?? c.targetUrl ?? "";
|
|
368
|
+
const jobId = jobIdOf(url);
|
|
369
|
+
if (jobId || checkDrillFor(url))
|
|
370
|
+
setLog({ repo: item.repo, jobId: jobId ?? "", name: checkLabel(c), url });
|
|
371
|
+
else if (url) $`open ${url}`.catch(() => {
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
useInput(
|
|
375
|
+
(input, key) => {
|
|
376
|
+
if (menu.handleKey(key)) return;
|
|
377
|
+
if (key.escape || input === "q") return onBack();
|
|
378
|
+
if (input === "M") {
|
|
379
|
+
menu.open([
|
|
380
|
+
...(0, lib_exports.buildActions)(
|
|
381
|
+
item,
|
|
382
|
+
login,
|
|
383
|
+
() => {
|
|
384
|
+
},
|
|
385
|
+
void 0,
|
|
386
|
+
void 0,
|
|
387
|
+
void 0,
|
|
388
|
+
onRefresh,
|
|
389
|
+
// onRemove navigates back itself, having stripped the row. Calling
|
|
390
|
+
// onBack() as well would re-set state from a closure captured
|
|
391
|
+
// before the removal, putting the row straight back.
|
|
392
|
+
(removed) => onRemove ? onRemove(removed) : onBack()
|
|
393
|
+
),
|
|
394
|
+
// Appended here rather than via gh-ink's extension seam: on this screen
|
|
395
|
+
// the launcher is mounted directly by `a` below, not opened as an overlay
|
|
396
|
+
// through onOpenExt, so there is no extension for buildActions to list.
|
|
397
|
+
// The browse screen gets the same entry the other way, from delegate's
|
|
398
|
+
// scope: "item" — same action, two hosts, two routes to it.
|
|
399
|
+
{
|
|
400
|
+
label: "Delegate to an agent",
|
|
401
|
+
hint: "a",
|
|
402
|
+
run: () => setAi(true)
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
label: "Copy prompt to clipboard",
|
|
406
|
+
hint: "y",
|
|
407
|
+
run: () => setCopy(true)
|
|
408
|
+
}
|
|
409
|
+
]);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (input === "o") {
|
|
413
|
+
$`open ${item.url}`.catch(() => {
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (input === "a") {
|
|
418
|
+
setAi(true);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (input === "y") {
|
|
422
|
+
setCopy(true);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (input === "e") {
|
|
426
|
+
setFiles(true);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (key.leftArrow || key.rightArrow)
|
|
430
|
+
setActive((t) => t === "health" ? "conversation" : "health");
|
|
431
|
+
},
|
|
432
|
+
{ isActive: inputActive }
|
|
433
|
+
);
|
|
434
|
+
if (files)
|
|
435
|
+
return /* @__PURE__ */ jsx(
|
|
436
|
+
FilePicker,
|
|
437
|
+
{
|
|
438
|
+
repo: item.repo,
|
|
439
|
+
number: item.number,
|
|
440
|
+
onBack: () => setFiles(false)
|
|
441
|
+
}
|
|
442
|
+
);
|
|
443
|
+
if (ai)
|
|
444
|
+
return /* @__PURE__ */ jsx(
|
|
445
|
+
AiLauncher,
|
|
446
|
+
{
|
|
447
|
+
item,
|
|
448
|
+
login,
|
|
449
|
+
prompt: seedPromptFor(item),
|
|
450
|
+
onBack: () => setAi(false)
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
if (copy)
|
|
454
|
+
return /* @__PURE__ */ jsx(CopyPromptNotice, { item, onBack: () => setCopy(false) });
|
|
455
|
+
if (log) {
|
|
456
|
+
const drill = checkDrillFor(log.url);
|
|
457
|
+
return drill ? /* @__PURE__ */ jsx(Fragment, { children: drill.render({
|
|
458
|
+
repo: log.repo,
|
|
459
|
+
url: log.url,
|
|
460
|
+
name: log.name,
|
|
461
|
+
onBack: () => setLog(null)
|
|
462
|
+
}) }) : /* @__PURE__ */ jsx(
|
|
463
|
+
CheckLogView,
|
|
464
|
+
{
|
|
465
|
+
repo: log.repo,
|
|
466
|
+
jobId: log.jobId,
|
|
467
|
+
name: log.name,
|
|
468
|
+
url: log.url,
|
|
469
|
+
onBack: () => setLog(null)
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
const hints = tab === "health" ? [
|
|
474
|
+
["\u2191\u2193", "nav"],
|
|
475
|
+
["\u21B5/l", "log"],
|
|
476
|
+
["r", "retrigger"],
|
|
477
|
+
["m", "merge"],
|
|
478
|
+
["a", "AI"],
|
|
479
|
+
["y", "copy prompt"],
|
|
480
|
+
["e", "files"],
|
|
481
|
+
["M", "actions"],
|
|
482
|
+
["\u2190\u2192", "tab"],
|
|
483
|
+
["o", "open PR"],
|
|
484
|
+
["q", "back"]
|
|
485
|
+
] : [
|
|
486
|
+
["\u2191\u2193", "thread"],
|
|
487
|
+
["x", "resolve"],
|
|
488
|
+
["r", "reply"],
|
|
489
|
+
["R", "show resolved"],
|
|
490
|
+
["M", "actions"],
|
|
491
|
+
["a", "AI"],
|
|
492
|
+
["y", "copy prompt"],
|
|
493
|
+
["\u2190\u2192", "tab"],
|
|
494
|
+
["q", "back"]
|
|
495
|
+
];
|
|
496
|
+
return /* @__PURE__ */ jsxs(
|
|
497
|
+
DrillView,
|
|
498
|
+
{
|
|
499
|
+
title: `#${item.number} \xB7 ${item.repo}`,
|
|
500
|
+
subtitle: item.title,
|
|
501
|
+
hints,
|
|
502
|
+
children: [
|
|
503
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Tabs, { active: tab, items: tabItems }) }),
|
|
504
|
+
menu.actions ? /* @__PURE__ */ jsx(lib_exports.ActionMenu, { item, actions: menu.actions, cursor: menu.cursor }) : tab === "health" ? /* @__PURE__ */ jsx(
|
|
505
|
+
HealthPanel,
|
|
506
|
+
{
|
|
507
|
+
repo: item.repo,
|
|
508
|
+
number: item.number,
|
|
509
|
+
data: health.data,
|
|
510
|
+
error: health.error,
|
|
511
|
+
reload: health.reload,
|
|
512
|
+
onOpenCheck,
|
|
513
|
+
onMerged: onMerged ? () => onMerged(item) : void 0
|
|
514
|
+
}
|
|
515
|
+
) : /* @__PURE__ */ jsx(
|
|
516
|
+
CommentsPanel,
|
|
517
|
+
{
|
|
518
|
+
repo: item.repo,
|
|
519
|
+
number: item.number,
|
|
520
|
+
data: comments.data,
|
|
521
|
+
error: comments.error,
|
|
522
|
+
reload: comments.reload,
|
|
523
|
+
onReplyingChange: setReplying,
|
|
524
|
+
showConversationHeading: false
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
]
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
};
|
|
531
|
+
var IssueView = ({
|
|
532
|
+
item,
|
|
533
|
+
login,
|
|
534
|
+
onBack
|
|
535
|
+
}) => {
|
|
536
|
+
const [replying, setReplying] = useState(false);
|
|
537
|
+
const [ai, setAi] = useState(false);
|
|
538
|
+
const [copy, setCopy] = useState(false);
|
|
539
|
+
const comments = useCachedResource(
|
|
540
|
+
`issue-comments-${item.repo}-${item.number}`,
|
|
541
|
+
() => fetchComments(item.repo, item.number, "issue")
|
|
542
|
+
);
|
|
543
|
+
const tabItems = [
|
|
544
|
+
{ value: "conversation", label: "Conversation" }
|
|
545
|
+
];
|
|
546
|
+
useInput(
|
|
547
|
+
(input, key) => {
|
|
548
|
+
if (key.escape || input === "q") return onBack();
|
|
549
|
+
if (input === "o") $`open ${item.url}`.catch(() => {
|
|
550
|
+
});
|
|
551
|
+
if (input === "a") setAi(true);
|
|
552
|
+
if (input === "y") setCopy(true);
|
|
553
|
+
},
|
|
554
|
+
{ isActive: !replying && !ai && !copy }
|
|
555
|
+
);
|
|
556
|
+
if (ai)
|
|
557
|
+
return /* @__PURE__ */ jsx(
|
|
558
|
+
AiLauncher,
|
|
559
|
+
{
|
|
560
|
+
item,
|
|
561
|
+
login,
|
|
562
|
+
prompt: seedPromptFor({ ...item, kind: "issue" }),
|
|
563
|
+
onBack: () => setAi(false)
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
if (copy)
|
|
567
|
+
return /* @__PURE__ */ jsx(
|
|
568
|
+
CopyPromptNotice,
|
|
569
|
+
{
|
|
570
|
+
item: { ...item, kind: "issue" },
|
|
571
|
+
onBack: () => setCopy(false)
|
|
572
|
+
}
|
|
573
|
+
);
|
|
574
|
+
return /* @__PURE__ */ jsxs(
|
|
575
|
+
DrillView,
|
|
576
|
+
{
|
|
577
|
+
title: `#${item.number} \xB7 ${item.repo}`,
|
|
578
|
+
subtitle: item.title,
|
|
579
|
+
hints: [
|
|
580
|
+
["\u2191\u2193", "scroll"],
|
|
581
|
+
["a", "AI"],
|
|
582
|
+
["y", "copy prompt"],
|
|
583
|
+
["o", "open in browser"],
|
|
584
|
+
["q/esc", "back"]
|
|
585
|
+
],
|
|
586
|
+
children: [
|
|
587
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Tabs, { active: "conversation", items: tabItems }) }),
|
|
588
|
+
/* @__PURE__ */ jsx(
|
|
589
|
+
CommentsPanel,
|
|
590
|
+
{
|
|
591
|
+
repo: item.repo,
|
|
592
|
+
number: item.number,
|
|
593
|
+
data: comments.data,
|
|
594
|
+
error: comments.error,
|
|
595
|
+
reload: comments.reload,
|
|
596
|
+
onReplyingChange: setReplying,
|
|
597
|
+
showConversationHeading: false
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
]
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
};
|
|
604
|
+
var detailFor = (ctx) => ctx.kind === "pr" ? /* @__PURE__ */ jsx(
|
|
605
|
+
PrView,
|
|
606
|
+
{
|
|
607
|
+
item: ctx.item,
|
|
608
|
+
login: ctx.login,
|
|
609
|
+
onBack: ctx.onBack,
|
|
610
|
+
onRefresh: ctx.onRefresh,
|
|
611
|
+
onRemove: ctx.onRemove,
|
|
612
|
+
onMerged: ctx.onMerged
|
|
613
|
+
}
|
|
614
|
+
) : (
|
|
615
|
+
// No refresh/remove/merged: IssueView has no action menu to hang them off,
|
|
616
|
+
// so passing them would type-check into a handler nothing ever calls. They
|
|
617
|
+
// belong here the day it grows the `M` menu PrView has.
|
|
618
|
+
/* @__PURE__ */ jsx(IssueView, { item: ctx.item, login: ctx.login, onBack: ctx.onBack })
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// src/index.ts
|
|
622
|
+
__reExport(index_exports, lib_exports);
|
|
623
|
+
|
|
624
|
+
export { IssueView, PrView, checkDrillFor, defineCockpit, detailFor, hasPatterns, parseArgs, registerCheckDrills };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kud/gh-cockpit",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A configurable GitHub cockpit for the terminal \u2014 your PRs, reviews and issues in one Ink TUI, grouped by whose move it is.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./extensions": {
|
|
14
|
+
"types": "./dist/extensions/index.d.ts",
|
|
15
|
+
"import": "./dist/extensions/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"github",
|
|
32
|
+
"pull-request",
|
|
33
|
+
"cli",
|
|
34
|
+
"ink",
|
|
35
|
+
"react",
|
|
36
|
+
"terminal",
|
|
37
|
+
"tui",
|
|
38
|
+
"cockpit",
|
|
39
|
+
"inbox"
|
|
40
|
+
],
|
|
41
|
+
"author": "Erwann Mest <m@kud.io>",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/kud/gh",
|
|
46
|
+
"directory": "packages/gh-cockpit"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"ink": ">=7",
|
|
53
|
+
"react": ">=19"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@kud/gh": "0.5.1",
|
|
57
|
+
"@kud/gh-ink": "0.21.0",
|
|
58
|
+
"@kud/ink-ui": "0.14.0",
|
|
59
|
+
"zx": "8.8.5"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "22.20.1",
|
|
63
|
+
"@types/react": "19.2.17",
|
|
64
|
+
"ink": "7.1.0",
|
|
65
|
+
"ink-testing-library": "4.0.0",
|
|
66
|
+
"react": "19.2.7",
|
|
67
|
+
"tsup": "8.5.1",
|
|
68
|
+
"typescript": "5.9.3",
|
|
69
|
+
"vitest": "4.1.10"
|
|
70
|
+
}
|
|
71
|
+
}
|