dsh-github-router 0.3.1 → 0.4.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/CHANGELOG.md +79 -56
- package/CONTRIBUTING.md +17 -6
- package/README.md +26 -18
- package/README.zh.md +22 -17
- package/docs/design.md +51 -43
- package/lib/client.js +165 -116
- package/lib/config.js +167 -116
- package/lib/core/issue.js +212 -202
- package/lib/core/pr.js +454 -429
- package/lib/index.js +11 -6
- package/lib/routes/gh.js +120 -109
- package/lib/routes/html.js +266 -250
- package/lib/settings.js +23 -21
- package/lib/tunnel.js +363 -247
- package/package.json +11 -17
package/lib/index.js
CHANGED
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
* 4. Plugin-owned state (fetch cache, response cache) lives under
|
|
20
20
|
* <DSH_HOME>/storages/dsh-github-router; user repositories are only ever
|
|
21
21
|
* read (log/diff/show) and only when explicitly granted.
|
|
22
|
-
* 5. Configuration
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* route exists.
|
|
22
|
+
* 5. Configuration is the exported `Config` schema (DSH ≥ 0.1.7-rc.2): the
|
|
23
|
+
* framework serves the `dsh-github-router` entry id as the settings
|
|
24
|
+
* namespace, derives the browser page from the schema, validates and
|
|
25
|
+
* persists writes, and redacts the secret token field on every wire
|
|
26
|
+
* boundary. The browser half renders that page in Settings → Plugins on
|
|
27
|
+
* the bundle's row, so no plugin-owned HTTP route exists.
|
|
28
28
|
* @module dsh-github-router
|
|
29
29
|
*/
|
|
30
30
|
import { installSettings } from './settings.js'
|
|
@@ -36,6 +36,11 @@ import { registerIssueTool } from './tools/issue.js'
|
|
|
36
36
|
import { registerPrTool } from './tools/pr.js'
|
|
37
37
|
import { registerProbeTool } from './tools/probe.js'
|
|
38
38
|
|
|
39
|
+
// The framework reads this entry's `Config` export as the settings schema and
|
|
40
|
+
// serves the entry id as its namespace; re-exporting keeps the schema in the
|
|
41
|
+
// module the Loader checks.
|
|
42
|
+
export { Config } from './config.js'
|
|
43
|
+
|
|
39
44
|
export const name = 'dsh-github-router'
|
|
40
45
|
export const inject = ['tools', 'subprocess', 'skills', 'systemPrompt']
|
|
41
46
|
|
package/lib/routes/gh.js
CHANGED
|
@@ -1,109 +1,120 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* gh CLI route. Only read-only subcommands are ever constructed here:
|
|
3
|
-
* `gh --version`, `gh auth status`, `gh api` (GET by default), and the
|
|
4
|
-
* `--json` viewers (`pr view`, `issue view`, `repo view`). JSON field lists
|
|
5
|
-
* are fixed constants — user input never reaches argv beyond validated
|
|
6
|
-
* owner/repo/number/path values, and nothing is ever passed through a shell.
|
|
7
|
-
* @module dsh-github-router/routes/gh
|
|
8
|
-
*/
|
|
9
|
-
import { childEnv, runCaptured } from '../util.js'
|
|
10
|
-
|
|
11
|
-
const PR_FIELDS =
|
|
12
|
-
'author,title,body,state,isDraft,mergedAt,additions,deletions,changedFiles,' +
|
|
13
|
-
'createdAt,updatedAt,baseRefName,headRefName,headRefOid,baseRefOid,' +
|
|
14
|
-
'mergeable,reviewDecision,reviews,comments,commits,files'
|
|
15
|
-
const ISSUE_FIELDS = 'author,title,body,state,stateReason,createdAt,updatedAt,closedAt,comments,labels,assignees'
|
|
16
|
-
const REPO_FIELDS = 'nameWithOwner,description,defaultBranchRef,stargazerCount,forkCount,updatedAt,licenseInfo,isPrivate'
|
|
17
|
-
|
|
18
|
-
export async function ghVersion(subprocess, signal) {
|
|
19
|
-
const out = await runCaptured(subprocess, { argv: ['gh', '--version'], graceMs: 8_000, signal, env: childEnv(), maxStdout: 4096, maxStderr: 4096 })
|
|
20
|
-
return out.ok
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function ghAuthed(subprocess, signal) {
|
|
24
|
-
const out = await runCaptured(subprocess, { argv: ['gh', 'auth', 'status'], graceMs: 8_000, signal, env: childEnv(), maxStdout: 8192, maxStderr: 8192 })
|
|
25
|
-
return out.ok
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Run a fixed gh invocation and parse its JSON stdout. */
|
|
29
|
-
async function ghJson(subprocess, argv, spec = {}) {
|
|
30
|
-
const out = await runCaptured(subprocess, {
|
|
31
|
-
argv,
|
|
32
|
-
graceMs: spec.graceMs ?? 30_000,
|
|
33
|
-
signal: spec.signal,
|
|
34
|
-
env: childEnv(),
|
|
35
|
-
maxStdout: spec.maxStdout ?? 1048576,
|
|
36
|
-
maxStderr: 262144,
|
|
37
|
-
})
|
|
38
|
-
if (!out.ok) {
|
|
39
|
-
const err = new Error(`gh exited with code ${out.exitCode}: ${out.stderr.text.slice(0, 400)}`)
|
|
40
|
-
err.code = 'GH_FAILED'
|
|
41
|
-
throw err
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(out.stdout.text)
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const err = new Error(`gh produced unprocessable JSON: ${String(error && error.message ? error.message : error)}`)
|
|
47
|
-
err.code = 'GH_FAILED'
|
|
48
|
-
throw err
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
1
|
+
/**
|
|
2
|
+
* gh CLI route. Only read-only subcommands are ever constructed here:
|
|
3
|
+
* `gh --version`, `gh auth status`, `gh api` (GET by default), and the
|
|
4
|
+
* `--json` viewers (`pr view`, `issue view`, `repo view`). JSON field lists
|
|
5
|
+
* are fixed constants — user input never reaches argv beyond validated
|
|
6
|
+
* owner/repo/number/path values, and nothing is ever passed through a shell.
|
|
7
|
+
* @module dsh-github-router/routes/gh
|
|
8
|
+
*/
|
|
9
|
+
import { childEnv, runCaptured } from '../util.js'
|
|
10
|
+
|
|
11
|
+
const PR_FIELDS =
|
|
12
|
+
'author,title,body,state,isDraft,mergedAt,additions,deletions,changedFiles,' +
|
|
13
|
+
'createdAt,updatedAt,baseRefName,headRefName,headRefOid,baseRefOid,' +
|
|
14
|
+
'mergeable,reviewDecision,reviews,comments,commits,files,url'
|
|
15
|
+
const ISSUE_FIELDS = 'author,title,body,state,stateReason,createdAt,updatedAt,closedAt,comments,labels,assignees,url'
|
|
16
|
+
const REPO_FIELDS = 'nameWithOwner,description,defaultBranchRef,stargazerCount,forkCount,updatedAt,licenseInfo,isPrivate'
|
|
17
|
+
|
|
18
|
+
export async function ghVersion(subprocess, signal) {
|
|
19
|
+
const out = await runCaptured(subprocess, { argv: ['gh', '--version'], graceMs: 8_000, signal, env: childEnv(), maxStdout: 4096, maxStderr: 4096 })
|
|
20
|
+
return out.ok
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function ghAuthed(subprocess, signal) {
|
|
24
|
+
const out = await runCaptured(subprocess, { argv: ['gh', 'auth', 'status'], graceMs: 8_000, signal, env: childEnv(), maxStdout: 8192, maxStderr: 8192 })
|
|
25
|
+
return out.ok
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Run a fixed gh invocation and parse its JSON stdout. */
|
|
29
|
+
async function ghJson(subprocess, argv, spec = {}) {
|
|
30
|
+
const out = await runCaptured(subprocess, {
|
|
31
|
+
argv,
|
|
32
|
+
graceMs: spec.graceMs ?? 30_000,
|
|
33
|
+
signal: spec.signal,
|
|
34
|
+
env: childEnv(),
|
|
35
|
+
maxStdout: spec.maxStdout ?? 1048576,
|
|
36
|
+
maxStderr: 262144,
|
|
37
|
+
})
|
|
38
|
+
if (!out.ok) {
|
|
39
|
+
const err = new Error(`gh exited with code ${out.exitCode}: ${out.stderr.text.slice(0, 400)}`)
|
|
40
|
+
err.code = 'GH_FAILED'
|
|
41
|
+
throw err
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(out.stdout.text)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const err = new Error(`gh produced unprocessable JSON: ${String(error && error.message ? error.message : error)}`)
|
|
47
|
+
err.code = 'GH_FAILED'
|
|
48
|
+
throw err
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `gh pr view <number> --repo <owner/repo> --json ...`
|
|
54
|
+
*
|
|
55
|
+
* The number is positional and the repository is passed with `--repo`:
|
|
56
|
+
* `gh pr view` also accepts a branch, so an `owner/repo/N` positional would
|
|
57
|
+
* be resolved as a branch against the local git repository instead.
|
|
58
|
+
*/
|
|
59
|
+
export async function ghPrView(subprocess, owner, repo, number, spec = {}) {
|
|
60
|
+
return ghJson(subprocess, ['gh', 'pr', 'view', String(number), '--repo', `${owner}/${repo}`, '--json', PR_FIELDS], spec)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `gh pr diff <number> --repo <owner/repo>` — capped text of the PR diff. */
|
|
64
|
+
export async function ghPrDiff(subprocess, owner, repo, number, spec = {}) {
|
|
65
|
+
const out = await runCaptured(subprocess, {
|
|
66
|
+
argv: ['gh', 'pr', 'diff', String(number), '--repo', `${owner}/${repo}`],
|
|
67
|
+
graceMs: 30_000,
|
|
68
|
+
signal: spec.signal,
|
|
69
|
+
env: childEnv(),
|
|
70
|
+
maxStdout: spec.maxStdout ?? 1048576,
|
|
71
|
+
maxStderr: 262144,
|
|
72
|
+
})
|
|
73
|
+
if (!out.ok) {
|
|
74
|
+
const err = new Error(`gh pr diff exited with code ${out.exitCode}: ${out.stderr.text.slice(0, 400)}`)
|
|
75
|
+
err.code = 'GH_FAILED'
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
return { text: out.stdout.text, truncated: out.stdout.truncated }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `gh issue view <number> --repo <owner/repo> --json ...`
|
|
83
|
+
*
|
|
84
|
+
* `gh issue view` accepts only a bare number or a URL, so the repository
|
|
85
|
+
* must travel in `--repo` — never as part of the positional argument.
|
|
86
|
+
*/
|
|
87
|
+
export async function ghIssueView(subprocess, owner, repo, number, spec = {}) {
|
|
88
|
+
return ghJson(subprocess, ['gh', 'issue', 'view', String(number), '--repo', `${owner}/${repo}`, '--json', ISSUE_FIELDS], spec)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `gh repo view <owner/repo> --json ...` */
|
|
92
|
+
export async function ghRepoView(subprocess, owner, repo, spec = {}) {
|
|
93
|
+
return ghJson(subprocess, ['gh', 'repo', 'view', `${owner}/${repo}`, '--json', REPO_FIELDS], spec)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `gh api <path>` — GET only, with the validated api path as a single argv
|
|
98
|
+
* element. Output is parsed as JSON when possible, else returned as text.
|
|
99
|
+
*/
|
|
100
|
+
export async function ghApi(subprocess, path, spec = {}) {
|
|
101
|
+
const out = await runCaptured(subprocess, {
|
|
102
|
+
argv: ['gh', 'api', path],
|
|
103
|
+
graceMs: 30_000,
|
|
104
|
+
signal: spec.signal,
|
|
105
|
+
env: childEnv(),
|
|
106
|
+
maxStdout: spec.maxStdout ?? 1048576,
|
|
107
|
+
maxStderr: 262144,
|
|
108
|
+
})
|
|
109
|
+
if (!out.ok) {
|
|
110
|
+
const err = new Error(`gh api exited with code ${out.exitCode}: ${out.stderr.text.slice(0, 400)}`)
|
|
111
|
+
err.code = 'GH_FAILED'
|
|
112
|
+
throw err
|
|
113
|
+
}
|
|
114
|
+
const text = out.stdout.text
|
|
115
|
+
try {
|
|
116
|
+
return { json: JSON.parse(text), truncated: out.stdout.truncated }
|
|
117
|
+
} catch {
|
|
118
|
+
return { json: null, text, truncated: out.stdout.truncated }
|
|
119
|
+
}
|
|
120
|
+
}
|