dsh-github-router 0.3.1 → 0.3.2
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/CHANGELOG.md +70 -56
- package/docs/design.md +5 -3
- package/lib/core/issue.js +212 -202
- package/lib/core/pr.js +454 -429
- package/lib/routes/gh.js +120 -109
- package/lib/routes/html.js +266 -250
- package/lib/tunnel.js +363 -247
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,56 +1,70 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project are documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [0.3.
|
|
9
|
-
|
|
10
|
-
### Fixed
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
## [0.1
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.2] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The `gh` route now builds valid arguments for its viewers.
|
|
13
|
+
- The `gh` route now returns the metadata URL field.
|
|
14
|
+
- The proxy transport now decodes chunked responses.
|
|
15
|
+
- The html route now handles the current GitHub pull request page payload.
|
|
16
|
+
- Fallback routes are now entered only when a part could not be fetched.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Offline regression tests for the `gh` route, chunked decoding, and failover gating.
|
|
21
|
+
|
|
22
|
+
## [0.3.1] - 2026-08-28
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Boot with DSH 0.1.2-alpha.4
|
|
27
|
+
|
|
28
|
+
## [0.3.0] - 2026-08-28
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Adapted to DeepSeek Harness 0.1.2-alpha.1
|
|
33
|
+
|
|
34
|
+
## [0.2.1] - 2026-08-28
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Declare compatibility for dsh 0.1.1-rc.2
|
|
39
|
+
|
|
40
|
+
## [0.2.0] - 2026-08-21
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Migrated plugin configuration to the framework's plugin-settings mechanism** (requires DSH ≥ 0.1.0-rc.7, which serves every registered settings namespace and dispatches plugin-owned configuration cards). The browser half now registers a `settings.plugin.item` card keyed by the `dsh-github-router` namespace inside Settings → Plugins, bound through `ctx.settingsScope` (shared describe mirror, revision fencing, recovery reads, reconnect invalidation); the plugin-owned `/dsh-github-router/config` web routes and the standalone `settings.section` page are removed. The Host keeps the same `settings.register` seam and adds `applies: 'live'`.
|
|
45
|
+
- The token control is now a true write-only field: the configured state comes from the describe mirror's secret slot list (badge: "已配置/configured"), a typed value writes the token, and a blank save clears a configured one.
|
|
46
|
+
- Runtime requirements: the settings card needs DSH ≥ 0.1.0-rc.7; the Host-side settings seam alone still works on older compositions (tools resolve the composition config as before).
|
|
47
|
+
|
|
48
|
+
## [0.1.0] - 2026-08-19
|
|
49
|
+
|
|
50
|
+
First release.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- Five read-only model tools: `github_probe`, `github_pr`, `github_issue`, `github_file`, `github_api`
|
|
55
|
+
- In-tool route ladder: api.github.com (direct → CONNECT-tunnel proxy) → gh CLI (read-only subcommands) → git protocol (plugin fetch cache + read-only local repo reads) → PR/issue page HTML (strict `react-app.embeddedData` JSON extraction) → user-configured raw mirrors (off by default), with per-part route attribution on every result
|
|
56
|
+
- `github_probe` connectivity matrix with per-route timings and a recommended route chain
|
|
57
|
+
- TTL response cache with atomic writes and corruption-tolerant reads
|
|
58
|
+
- Settings: the `dsh-github-router` namespace on the official settings seam (secret token / `tokenEnv` credential ref, proxy, route switches, cache TTLs, byte caps, granted local repos), an independent Settings page (the dsh-notification `settings.section` mechanism; common fields up top, the long tail in a collapsed "Advanced settings" disclosure), and plugin-owned `/dsh-github-router/config` web routes (the dsh-market pattern) with same-origin enforcement and revision fencing
|
|
59
|
+
- `github-router` skill and a system-prompt guidance section (order 118)
|
|
60
|
+
- Zero runtime dependencies: zero-dependency CONNECT tunnel for proxied requests; peers resolve from the DSH profile
|
|
61
|
+
- 72 offline tests (`node --test`) covering extraction, guards, cache, tunnel, tool wiring, client bundle, config routes, and localization compliance
|
|
62
|
+
- Documentation: bilingual README, SECURITY, design notes, and a development guide
|
|
63
|
+
|
|
64
|
+
### Security
|
|
65
|
+
|
|
66
|
+
- Read-only by construction: no write/push/comment/mutation verb exists anywhere
|
|
67
|
+
- Tokens attach only to api.github.com and are redacted on every wire boundary
|
|
68
|
+
- Page HTML is parsed with `JSON.parse` only (never evaluated); a bounded BFS copies a whitelist of fields
|
|
69
|
+
- Subprocess calls are argv arrays with fixed flag lists — no shell interpolation
|
|
70
|
+
- Configuration writes require same-origin POSTs and are body-capped
|
package/docs/design.md
CHANGED
|
@@ -189,9 +189,11 @@ within one host process.
|
|
|
189
189
|
|
|
190
190
|
## Known limitations
|
|
191
191
|
|
|
192
|
-
- **Fallback routes are
|
|
193
|
-
|
|
194
|
-
(
|
|
192
|
+
- **Fallback routes are failover, not fan-out.** A ladder is entered only
|
|
193
|
+
when a part could not be fetched by the route above it: a successfully
|
|
194
|
+
fetched empty list (an issue with no comments, a PR with no reviews) is
|
|
195
|
+
authoritative and does not trigger gh/html/git, saving quota. Forcing a
|
|
196
|
+
specific route per call is not yet supported.
|
|
195
197
|
- **Anonymous quota is shared per IP** (60 req/h). A PR aggregation costs
|
|
196
198
|
up to six calls. Token configuration is the intended mitigation; the
|
|
197
199
|
cache is the second line.
|
package/lib/core/issue.js
CHANGED
|
@@ -1,202 +1,212 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue aggregation: api.github.com → gh CLI → page HTML, with per-part
|
|
3
|
-
* route attribution and structured failure.
|
|
4
|
-
* @module dsh-github-router/core/issue
|
|
5
|
-
*/
|
|
6
|
-
import { ghAuthed, ghIssueView, ghVersion } from '../routes/gh.js'
|
|
7
|
-
import { fetchAndParsePage } from '../routes/html.js'
|
|
8
|
-
import { guardByteCount, guardOwnerRepo, guardPrNumber, truncateText } from '../util.js'
|
|
9
|
-
import { attemptLadder, buildRuntime, rateNote } from './runtime.js'
|
|
10
|
-
|
|
11
|
-
const BODY_CAP = 20000
|
|
12
|
-
|
|
13
|
-
function shapeApiIssue(meta, owner, repo, number) {
|
|
14
|
-
return {
|
|
15
|
-
owner,
|
|
16
|
-
repo,
|
|
17
|
-
number,
|
|
18
|
-
title: meta.title ?? null,
|
|
19
|
-
body: truncateText(meta.body ?? '', BODY_CAP).text,
|
|
20
|
-
state: meta.state ?? null,
|
|
21
|
-
stateReason: meta.state_reason ?? null,
|
|
22
|
-
author: meta.user && meta.user.login ? meta.user.login : null,
|
|
23
|
-
createdAt: meta.created_at ?? null,
|
|
24
|
-
updatedAt: meta.updated_at ?? null,
|
|
25
|
-
closedAt: meta.closed_at ?? null,
|
|
26
|
-
commentsCount: meta.comments ?? null,
|
|
27
|
-
labels: Array.isArray(meta.labels) ? meta.labels.map((l) => l.name ?? null).filter((n) => n !== null) : [],
|
|
28
|
-
url: meta.html_url ?? null,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function shapeGhIssue(json, owner, repo, number) {
|
|
33
|
-
return {
|
|
34
|
-
owner,
|
|
35
|
-
repo,
|
|
36
|
-
number,
|
|
37
|
-
title: json.title ?? null,
|
|
38
|
-
body: truncateText(json.body ?? '', BODY_CAP).text,
|
|
39
|
-
state: json.state ?? null,
|
|
40
|
-
stateReason: json.stateReason ?? null,
|
|
41
|
-
author: json.author && json.author.login ? json.author.login : null,
|
|
42
|
-
createdAt: json.createdAt ?? null,
|
|
43
|
-
updatedAt: json.updatedAt ?? null,
|
|
44
|
-
closedAt: json.closedAt ?? null,
|
|
45
|
-
commentsCount: Array.isArray(json.comments) ? json.comments.length : null,
|
|
46
|
-
labels: Array.isArray(json.labels) ? json.labels.map((l) => (typeof l === 'string' ? l : l.name ?? null)).filter((n) => n !== null) : [],
|
|
47
|
-
url: null,
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function capItems(items, maxItems, notes, label) {
|
|
52
|
-
if (items.length <= maxItems) return items
|
|
53
|
-
notes.push(`${label}: showing ${maxItems} of ${items.length}`)
|
|
54
|
-
return items.slice(0, maxItems)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function aggregateIssue(ctx, options, args) {
|
|
58
|
-
const owner = guardOwnerRepo(args.owner, 'owner')
|
|
59
|
-
const repo = guardOwnerRepo(args.repo, 'repo')
|
|
60
|
-
const number = guardPrNumber(args.number, 'number')
|
|
61
|
-
const maxComments = guardByteCount(args.maxComments, 'maxComments', 50, 5, 200)
|
|
62
|
-
const forceRefresh = args.forceRefresh === true
|
|
63
|
-
const { signal } = args
|
|
64
|
-
|
|
65
|
-
const runtime = buildRuntime(ctx, options)
|
|
66
|
-
const { subprocess, fetchImpl, cache, api } = runtime
|
|
67
|
-
const token = await runtime.resolveToken()
|
|
68
|
-
const state = {
|
|
69
|
-
issue: null,
|
|
70
|
-
comments: [],
|
|
71
|
-
routes: { meta: null, comments: null },
|
|
72
|
-
notes: [],
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
state.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Issue aggregation: api.github.com → gh CLI → page HTML, with per-part
|
|
3
|
+
* route attribution and structured failure.
|
|
4
|
+
* @module dsh-github-router/core/issue
|
|
5
|
+
*/
|
|
6
|
+
import { ghAuthed, ghIssueView, ghVersion } from '../routes/gh.js'
|
|
7
|
+
import { fetchAndParsePage } from '../routes/html.js'
|
|
8
|
+
import { guardByteCount, guardOwnerRepo, guardPrNumber, truncateText } from '../util.js'
|
|
9
|
+
import { attemptLadder, buildRuntime, rateNote } from './runtime.js'
|
|
10
|
+
|
|
11
|
+
const BODY_CAP = 20000
|
|
12
|
+
|
|
13
|
+
function shapeApiIssue(meta, owner, repo, number) {
|
|
14
|
+
return {
|
|
15
|
+
owner,
|
|
16
|
+
repo,
|
|
17
|
+
number,
|
|
18
|
+
title: meta.title ?? null,
|
|
19
|
+
body: truncateText(meta.body ?? '', BODY_CAP).text,
|
|
20
|
+
state: meta.state ?? null,
|
|
21
|
+
stateReason: meta.state_reason ?? null,
|
|
22
|
+
author: meta.user && meta.user.login ? meta.user.login : null,
|
|
23
|
+
createdAt: meta.created_at ?? null,
|
|
24
|
+
updatedAt: meta.updated_at ?? null,
|
|
25
|
+
closedAt: meta.closed_at ?? null,
|
|
26
|
+
commentsCount: meta.comments ?? null,
|
|
27
|
+
labels: Array.isArray(meta.labels) ? meta.labels.map((l) => l.name ?? null).filter((n) => n !== null) : [],
|
|
28
|
+
url: meta.html_url ?? null,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shapeGhIssue(json, owner, repo, number) {
|
|
33
|
+
return {
|
|
34
|
+
owner,
|
|
35
|
+
repo,
|
|
36
|
+
number,
|
|
37
|
+
title: json.title ?? null,
|
|
38
|
+
body: truncateText(json.body ?? '', BODY_CAP).text,
|
|
39
|
+
state: json.state ?? null,
|
|
40
|
+
stateReason: json.stateReason ?? null,
|
|
41
|
+
author: json.author && json.author.login ? json.author.login : null,
|
|
42
|
+
createdAt: json.createdAt ?? null,
|
|
43
|
+
updatedAt: json.updatedAt ?? null,
|
|
44
|
+
closedAt: json.closedAt ?? null,
|
|
45
|
+
commentsCount: Array.isArray(json.comments) ? json.comments.length : null,
|
|
46
|
+
labels: Array.isArray(json.labels) ? json.labels.map((l) => (typeof l === 'string' ? l : l.name ?? null)).filter((n) => n !== null) : [],
|
|
47
|
+
url: json.url ?? null,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function capItems(items, maxItems, notes, label) {
|
|
52
|
+
if (items.length <= maxItems) return items
|
|
53
|
+
notes.push(`${label}: showing ${maxItems} of ${items.length}`)
|
|
54
|
+
return items.slice(0, maxItems)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function aggregateIssue(ctx, options, args) {
|
|
58
|
+
const owner = guardOwnerRepo(args.owner, 'owner')
|
|
59
|
+
const repo = guardOwnerRepo(args.repo, 'repo')
|
|
60
|
+
const number = guardPrNumber(args.number, 'number')
|
|
61
|
+
const maxComments = guardByteCount(args.maxComments, 'maxComments', 50, 5, 200)
|
|
62
|
+
const forceRefresh = args.forceRefresh === true
|
|
63
|
+
const { signal } = args
|
|
64
|
+
|
|
65
|
+
const runtime = buildRuntime(ctx, options)
|
|
66
|
+
const { subprocess, fetchImpl, cache, api } = runtime
|
|
67
|
+
const token = await runtime.resolveToken()
|
|
68
|
+
const state = {
|
|
69
|
+
issue: null,
|
|
70
|
+
comments: [],
|
|
71
|
+
routes: { meta: null, comments: null },
|
|
72
|
+
notes: [],
|
|
73
|
+
// Which parts a route actually served. A successfully fetched empty
|
|
74
|
+
// list is authoritative and must NOT trigger a fallback ladder — the
|
|
75
|
+
// routes are failover, not fan-out (docs/design.md).
|
|
76
|
+
fetched: { meta: false, comments: false },
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.routes.api) {
|
|
80
|
+
try {
|
|
81
|
+
const attempt = await attemptLadder(
|
|
82
|
+
(proxy) =>
|
|
83
|
+
api.getJson(`/repos/${owner}/${repo}/issues/${number}`, {
|
|
84
|
+
token,
|
|
85
|
+
signal,
|
|
86
|
+
proxy,
|
|
87
|
+
cache,
|
|
88
|
+
ttlMs: options.cacheTtlSeconds.meta,
|
|
89
|
+
forceRefresh,
|
|
90
|
+
fetchImpl,
|
|
91
|
+
}),
|
|
92
|
+
options,
|
|
93
|
+
)
|
|
94
|
+
state.issue = shapeApiIssue(attempt.value, owner, repo, number)
|
|
95
|
+
state.routes.meta = 'api'
|
|
96
|
+
state.fetched.meta = true
|
|
97
|
+
rateNote(attempt.value._headers, state.notes)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
state.notes.push(`api issue: ${String(error && error.message ? error.message : error).slice(0, 220)}`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state.issue !== null) {
|
|
104
|
+
try {
|
|
105
|
+
const attempt = await attemptLadder(
|
|
106
|
+
(proxy) =>
|
|
107
|
+
api.getJson(`/repos/${owner}/${repo}/issues/${number}/comments`, {
|
|
108
|
+
token,
|
|
109
|
+
signal,
|
|
110
|
+
proxy,
|
|
111
|
+
cache,
|
|
112
|
+
ttlMs: options.cacheTtlSeconds.meta,
|
|
113
|
+
forceRefresh,
|
|
114
|
+
fetchImpl,
|
|
115
|
+
query: { per_page: 100 },
|
|
116
|
+
}),
|
|
117
|
+
options,
|
|
118
|
+
)
|
|
119
|
+
state.comments = capItems(
|
|
120
|
+
(Array.isArray(attempt.value) ? attempt.value : []).map((c) => ({
|
|
121
|
+
kind: 'comment',
|
|
122
|
+
author: c.user && c.user.login ? c.user.login : null,
|
|
123
|
+
createdAt: c.created_at ?? null,
|
|
124
|
+
body: truncateText(c.body ?? '', BODY_CAP).text,
|
|
125
|
+
url: c.html_url ?? null,
|
|
126
|
+
})),
|
|
127
|
+
maxComments,
|
|
128
|
+
state.notes,
|
|
129
|
+
'comments',
|
|
130
|
+
)
|
|
131
|
+
state.routes.comments = 'api'
|
|
132
|
+
state.fetched.comments = true
|
|
133
|
+
} catch (error) {
|
|
134
|
+
state.notes.push(`api comments: ${String(error && error.message ? error.message : error).slice(0, 180)}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.routes.gh && subprocess !== undefined && (state.issue === null || !state.fetched.comments)) {
|
|
139
|
+
let ghUsable = false
|
|
140
|
+
try {
|
|
141
|
+
ghUsable = (await ghVersion(subprocess, signal)) && (await ghAuthed(subprocess, signal))
|
|
142
|
+
} catch { ghUsable = false }
|
|
143
|
+
if (ghUsable) {
|
|
144
|
+
try {
|
|
145
|
+
const g = await ghIssueView(subprocess, owner, repo, number, { signal })
|
|
146
|
+
if (state.issue === null) {
|
|
147
|
+
state.issue = shapeGhIssue(g, owner, repo, number)
|
|
148
|
+
state.routes.meta = 'gh'
|
|
149
|
+
state.fetched.meta = true
|
|
150
|
+
}
|
|
151
|
+
if (!state.fetched.comments && Array.isArray(g.comments)) {
|
|
152
|
+
state.comments = capItems(
|
|
153
|
+
g.comments.map((c) => ({
|
|
154
|
+
kind: 'comment',
|
|
155
|
+
author: c.author && c.author.login ? c.author.login : null,
|
|
156
|
+
createdAt: c.createdAt ?? null,
|
|
157
|
+
body: truncateText(c.body ?? '', BODY_CAP).text,
|
|
158
|
+
url: c.url ?? null,
|
|
159
|
+
})),
|
|
160
|
+
maxComments,
|
|
161
|
+
state.notes,
|
|
162
|
+
'comments',
|
|
163
|
+
)
|
|
164
|
+
state.routes.comments = 'gh'
|
|
165
|
+
state.fetched.comments = true
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
state.notes.push(`gh issue view: ${String(error && error.message ? error.message : error).slice(0, 180)}`)
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
state.notes.push('gh route skipped: gh CLI not installed or not authenticated')
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.routes.html && (state.issue === null || !state.fetched.comments)) {
|
|
176
|
+
try {
|
|
177
|
+
await attemptLadder(
|
|
178
|
+
async (proxy) => {
|
|
179
|
+
const parsed = await fetchAndParsePage(owner, repo, number, 'issues', {
|
|
180
|
+
fetchImpl,
|
|
181
|
+
proxy,
|
|
182
|
+
signal,
|
|
183
|
+
timeoutMs: proxy !== undefined ? options.proxyTimeoutMs : options.directTimeoutMs,
|
|
184
|
+
})
|
|
185
|
+
if (state.issue === null && parsed.issue !== null) {
|
|
186
|
+
state.issue = { ...parsed.issue, owner, repo, number }
|
|
187
|
+
state.routes.meta = 'html'
|
|
188
|
+
state.fetched.meta = true
|
|
189
|
+
}
|
|
190
|
+
if (!state.fetched.comments && parsed.discussion.length > 0) {
|
|
191
|
+
state.comments = capItems(parsed.discussion, maxComments, state.notes, 'comments')
|
|
192
|
+
state.routes.comments = 'html'
|
|
193
|
+
state.fetched.comments = true
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
options,
|
|
197
|
+
{ proxyFirst: true },
|
|
198
|
+
)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
state.notes.push(`html page: ${String(error && error.message ? error.message : error).slice(0, 220)}`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (state.issue === null) {
|
|
205
|
+
return {
|
|
206
|
+
error: `all GitHub routes failed to load issue ${owner}/${repo}#${number}`,
|
|
207
|
+
routes: state.routes,
|
|
208
|
+
notes: state.notes,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { issue: state.issue, comments: state.comments, routes: state.routes, notes: state.notes }
|
|
212
|
+
}
|