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/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 rides the official settings seam end to end
23
- * (`dsh-github-router` namespace: durable document, schema validation,
24
- * revision fencing). Since DSH 0.1.0-rc.7 the framework serves every
25
- * registered settings namespace to the browser and renders the plugin's
26
- * configuration card in Settings → Plugins, so no plugin-owned HTTP
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
- /** `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
+ }