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/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
- /** `gh pr view <owner/repo/N> --json ...` */
53
- export async function ghPrView(subprocess, owner, repo, number, spec = {}) {
54
- return ghJson(subprocess, ['gh', 'pr', 'view', `${owner}/${repo}/${number}`, '--json', PR_FIELDS], spec)
55
- }
56
-
57
- /** `gh pr diff <owner/repo/N>` — capped text of the PR diff. */
58
- export async function ghPrDiff(subprocess, owner, repo, number, spec = {}) {
59
- const out = await runCaptured(subprocess, {
60
- argv: ['gh', 'pr', 'diff', `${owner}/${repo}/${number}`],
61
- graceMs: 30_000,
62
- signal: spec.signal,
63
- env: childEnv(),
64
- maxStdout: spec.maxStdout ?? 1048576,
65
- maxStderr: 262144,
66
- })
67
- if (!out.ok) {
68
- const err = new Error(`gh pr diff exited with code ${out.exitCode}: ${out.stderr.text.slice(0, 400)}`)
69
- err.code = 'GH_FAILED'
70
- throw err
71
- }
72
- return { text: out.stdout.text, truncated: out.stdout.truncated }
73
- }
74
-
75
- /** `gh issue view <owner/repo/N> --json ...` */
76
- export async function ghIssueView(subprocess, owner, repo, number, spec = {}) {
77
- return ghJson(subprocess, ['gh', 'issue', 'view', `${owner}/${repo}/${number}`, '--json', ISSUE_FIELDS], spec)
78
- }
79
-
80
- /** `gh repo view <owner/repo> --json ...` */
81
- export async function ghRepoView(subprocess, owner, repo, spec = {}) {
82
- return ghJson(subprocess, ['gh', 'repo', 'view', `${owner}/${repo}`, '--json', REPO_FIELDS], spec)
83
- }
84
-
85
- /**
86
- * `gh api <path>` — GET only, with the validated api path as a single argv
87
- * element. Output is parsed as JSON when possible, else returned as text.
88
- */
89
- export async function ghApi(subprocess, path, spec = {}) {
90
- const out = await runCaptured(subprocess, {
91
- argv: ['gh', 'api', path],
92
- graceMs: 30_000,
93
- signal: spec.signal,
94
- env: childEnv(),
95
- maxStdout: spec.maxStdout ?? 1048576,
96
- maxStderr: 262144,
97
- })
98
- if (!out.ok) {
99
- const err = new Error(`gh api exited with code ${out.exitCode}: ${out.stderr.text.slice(0, 400)}`)
100
- err.code = 'GH_FAILED'
101
- throw err
102
- }
103
- const text = out.stdout.text
104
- try {
105
- return { json: JSON.parse(text), truncated: out.stdout.truncated }
106
- } catch {
107
- return { json: null, text, truncated: out.stdout.truncated }
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
+ }