@goodandready/dsh-github-ops 0.6.8 → 0.6.10

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.10
4
+
5
+ ### Features
6
+ - **GitHub Environments & Deployments Management (#122)**: Added `gh_environment_list` tool to inspect deployment environments, wait timers, and protection rules. Added `gh_deployment_list`, `gh_deployment_create`, and `gh_deployment_status_create` tools for full deployment lifecycle auditing and status updates.
7
+ - **Webhook Simulator & Event Emitter (#125)**: Added `gh_webhook_simulate` tool supporting canonical mock payloads for `issues`, `pull_request`, `workflow_run`, `release`, `deployment`, `deployment_status`, and `push` events, with live broadcast into local `SseEventManager` and optional HTTP delivery to webhook receivers.
8
+
9
+ ## 0.6.9
10
+
11
+ ### Features
12
+ - **Conventional Changelog Generator (#123)**: Added `gh_changelog_generate` tool parsing commit and PR history according to Conventional Commits standard (`feat:`, `fix:`, `perf:`, `refactor:`, `chore:`, etc.), detecting Breaking Changes (`!` and `BREAKING CHANGE:`), recommending SemVer bump (`major`, `minor`, `patch`), and outputting structured JSON and GitHub/CHANGELOG markdown. Added Changelog tab in repository panel UI.
13
+ - **GoodAndReady Community Scaffold (#126)**: Added `gh_community_scaffold` tool generating standardized community health files (`.github/ISSUE_TEMPLATE` bug report, feature request, security advisory; 5-section `.github/PULL_REQUEST_TEMPLATE.md`; `SECURITY.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`). Integrated `includeCommunityFiles` option into `gh_upload_project` and `uploadProject`.
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Features
package/lib/client.js CHANGED
@@ -130,6 +130,9 @@ window.__ModuleLoader__.load({
130
130
  panelSecurity: 'Security',
131
131
  panelSecurityEmpty: 'No security alerts found.',
132
132
  panelSecurityDisabled: 'Security features are disabled or token lacks security scope.',
133
+ panelChangelog: 'Changelog',
134
+ panelChangelogEmpty: 'No conventional changelog entries found.',
135
+ panelCopyMarkdown: 'Copy Markdown',
133
136
  panelDiagnose: 'Diagnose',
134
137
  panelDiagnosing: 'Diagnosing…',
135
138
  panelDiagnoseAsk: 'Ask Agent to Diagnose & Fix',
@@ -257,6 +260,9 @@ window.__ModuleLoader__.load({
257
260
  panelSecurity: '安全',
258
261
  panelSecurityEmpty: '未发现安全警报。',
259
262
  panelSecurityDisabled: '安全功能已禁用或令牌缺少安全权限。',
263
+ panelChangelog: '更新日志',
264
+ panelChangelogEmpty: '未找到常规提交更新日志记录。',
265
+ panelCopyMarkdown: '复制 Markdown',
260
266
  panelDiagnose: '诊断',
261
267
  panelDiagnosing: '诊断中…',
262
268
  panelDiagnoseAsk: '请智能体诊断并修复',
@@ -870,6 +876,7 @@ window.__ModuleLoader__.load({
870
876
  const [error, setError] = React.useState('')
871
877
  const [loading, setLoading] = React.useState(false)
872
878
  const [securityData, setSecurityData] = React.useState({ phase: 'idle', data: null })
879
+ const [changelogData, setChangelogData] = React.useState({ phase: 'idle', data: null })
873
880
  const [diagnostics, setDiagnostics] = React.useState(null)
874
881
  const [diagnoseLoadingId, setDiagnoseLoadingId] = React.useState(null)
875
882
  const [expandedSteps, setExpandedSteps] = React.useState({})
@@ -921,6 +928,14 @@ window.__ModuleLoader__.load({
921
928
  .catch((err) => { setSecurityData({ phase: 'error', data: null }); setError(String((err && err.message) || err)) })
922
929
  }, [call, repo])
923
930
 
931
+ const loadChangelog = React.useCallback(() => {
932
+ if (!repo) return
933
+ setChangelogData({ phase: 'loading', data: null })
934
+ call({ what: 'changelog', repo })
935
+ .then((payload) => setChangelogData({ phase: 'ready', data: payload }))
936
+ .catch((err) => { setChangelogData({ phase: 'error', data: null }); setError(String((err && err.message) || err)) })
937
+ }, [call, repo])
938
+
924
939
  const loadDiagnostics = (runId) => {
925
940
  if (!repo || !runId) return
926
941
  setDiagnoseLoadingId(runId)
@@ -957,8 +972,9 @@ window.__ModuleLoader__.load({
957
972
  else if (tab === 'inbox') loadInbox()
958
973
  else if (tab === 'matrix') loadMatrix()
959
974
  else if (tab === 'security') loadSecurity()
975
+ else if (tab === 'changelog') loadChangelog()
960
976
  else loadList(tab)
961
- }, [repo, tab, loadTree, loadList, loadInbox, loadSecurity])
977
+ }, [repo, tab, loadTree, loadList, loadInbox, loadSecurity, loadChangelog])
962
978
 
963
979
  const openFile = (entry) => {
964
980
  if (entry.type === 'tree') { loadTree(entry.path); return }
@@ -1143,6 +1159,45 @@ window.__ModuleLoader__.load({
1143
1159
  )
1144
1160
  })()
1145
1161
 
1162
+ const changelogView = (() => {
1163
+ if (changelogData.phase === 'loading') return React.createElement('div', { className: 'gho-panel-muted', style: { padding: '8px 0' } }, t('panelLoading'))
1164
+ const data = changelogData.data
1165
+ if (!data || !data.markdown) return React.createElement('div', { className: 'gho-panel-muted', style: { padding: '8px 0' } }, t('panelChangelogEmpty'))
1166
+
1167
+ return React.createElement('div', { style: { padding: '4px 0' } },
1168
+ React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', margin: '4px 0 8px' } },
1169
+ React.createElement('span', { className: 'gho-badge gho-badge-active' }, `Bump: ${data.recommendedBump || 'none'}`),
1170
+ React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, data.version || ''),
1171
+ React.createElement('button', {
1172
+ type: 'button',
1173
+ className: 'gho-diagnose-btn',
1174
+ style: { marginLeft: 'auto' },
1175
+ onClick: () => {
1176
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
1177
+ navigator.clipboard.writeText(data.markdown || '').catch(() => {})
1178
+ }
1179
+ setCopiedPrompt(true)
1180
+ setTimeout(() => setCopiedPrompt(false), 3000)
1181
+ },
1182
+ }, copiedPrompt ? t('panelDiagnoseCopied') : t('panelCopyMarkdown')),
1183
+ ),
1184
+ React.createElement('pre', {
1185
+ style: {
1186
+ whiteSpace: 'pre-wrap',
1187
+ fontFamily: 'monospace',
1188
+ fontSize: '11px',
1189
+ lineHeight: '1.4',
1190
+ background: 'var(--dsw-alias-bg-layer-2)',
1191
+ border: '1px solid var(--dsw-alias-border-l2)',
1192
+ borderRadius: '6px',
1193
+ padding: '8px',
1194
+ maxHeight: '360px',
1195
+ overflow: 'auto',
1196
+ },
1197
+ }, data.markdown),
1198
+ )
1199
+ })()
1200
+
1146
1201
  return React.createElement('div', { className: 'gho-panel' },
1147
1202
  header,
1148
1203
  React.createElement('div', { className: 'gho-panel-tabs' },
@@ -1151,12 +1206,13 @@ window.__ModuleLoader__.load({
1151
1206
  tabButton('pulls', t('panelPulls')),
1152
1207
  tabButton('runs', t('panelRuns')),
1153
1208
  tabButton('security', t('panelSecurity')),
1209
+ tabButton('changelog', t('panelChangelog')),
1154
1210
  tabButton('inbox', t('panelInbox')),
1155
1211
  tabButton('matrix', t('panelMatrix')),
1156
1212
  React.createElement('button', { type: 'button', className: 'gho-panel-tab', onClick: () => { setRepo(''); setFile(null) } }, t('panelRepo')),
1157
1213
  ),
1158
1214
  error ? React.createElement('div', { className: 'gho-panel-muted' }, error) : null,
1159
- tab === 'tree' ? treeView : (tab === 'matrix' ? matrixView : (tab === 'security' ? securityView : list)),
1215
+ tab === 'tree' ? treeView : (tab === 'matrix' ? matrixView : (tab === 'security' ? securityView : (tab === 'changelog' ? changelogView : list))),
1160
1216
  )
1161
1217
  }
1162
1218
 
package/lib/events.js CHANGED
@@ -1,3 +1,14 @@
1
+
2
+ let activeEventManager = null
3
+
4
+ export function setGlobalEventManager(mgr) {
5
+ activeEventManager = mgr
6
+ }
7
+
8
+ export function getGlobalEventManager() {
9
+ return activeEventManager
10
+ }
11
+
1
12
  // Server-Sent Events (SSE) notification manager for CI runs and PR events.
2
13
  //
3
14
  // Maintains client subscriptions, broadcasts real-time events, and runs an adaptive
@@ -21,6 +32,7 @@ export class SseEventManager {
21
32
  this.lastRuns = new Map() // runId -> { status, conclusion }
22
33
  this.lastPulls = new Map() // pullNumber -> { updated, state }
23
34
  this.hasActiveWork = false
35
+ setGlobalEventManager(this)
24
36
  }
25
37
 
26
38
  get clientCount() {
@@ -197,5 +209,6 @@ export class SseEventManager {
197
209
  this.clients.clear()
198
210
  this.lastRuns.clear()
199
211
  this.lastPulls.clear()
212
+ if (getGlobalEventManager() === this) setGlobalEventManager(null)
200
213
  }
201
214
  }
package/lib/routes.js CHANGED
@@ -16,6 +16,7 @@ import { encodeRepoPath } from './github.js'
16
16
  import { listIssues } from './tools/issues.js'
17
17
  import { listRuns, diagnoseRun } from './tools/actions.js'
18
18
  import { listSecurityAlerts } from './tools/security.js'
19
+ import { generateChangelog } from './tools/changelog.js'
19
20
  import { listNotifications, repoTreeView } from './tools/panel-data.js'
20
21
  import { mirrorStatus, syncMirror } from './tools/mirror.js'
21
22
  import { matrixStatus } from './tools/matrix.js'
@@ -413,6 +414,12 @@ export function registerRoutes(ctx, deps = {}) {
413
414
  const result = await listRuns(github, { ...repo, limit: 20 })
414
415
  return sendJson(res, 200, { configured: true, items: result.runs })
415
416
  }
417
+ if (what === 'changelog') {
418
+ const base = url.searchParams.get('base') || undefined
419
+ const head = url.searchParams.get('head') || 'HEAD'
420
+ const result = await generateChangelog(github, { ...repo, base, head })
421
+ return sendJson(res, 200, { configured: true, ...result })
422
+ }
416
423
  if (what === 'security') {
417
424
  const severity = url.searchParams.get('severity') || undefined
418
425
  const result = await listSecurityAlerts(github, { ...repo, state, severity, limit: 50 })
@@ -0,0 +1,261 @@
1
+ // Conventional changelog generation and SemVer recommendation tools.
2
+ // Pure functions over GitHub client or commit arrays.
3
+
4
+ export const CATEGORIES = {
5
+ breaking: { key: 'breaking', title: '⚠️ Breaking Changes', order: 1 },
6
+ feat: { key: 'feat', title: '🚀 Features', order: 2 },
7
+ fix: { key: 'fix', title: '🐛 Bug Fixes', order: 3 },
8
+ perf: { key: 'perf', title: '⚡ Performance Improvements', order: 4 },
9
+ refactor: { key: 'refactor', title: '♻️ Code Refactoring', order: 5 },
10
+ docs: { key: 'docs', title: '📚 Documentation', order: 6 },
11
+ test: { key: 'test', title: '🧪 Tests', order: 7 },
12
+ maintenance: { key: 'maintenance', title: '🛠️ Maintenance', order: 8 },
13
+ other: { key: 'other', title: '📦 Other Changes', order: 9 },
14
+ }
15
+
16
+ const TYPE_TO_CATEGORY = {
17
+ feat: 'feat',
18
+ feature: 'feat',
19
+ fix: 'fix',
20
+ bugfix: 'fix',
21
+ perf: 'perf',
22
+ refactor: 'refactor',
23
+ docs: 'docs',
24
+ doc: 'docs',
25
+ test: 'test',
26
+ tests: 'test',
27
+ chore: 'maintenance',
28
+ ci: 'maintenance',
29
+ build: 'maintenance',
30
+ deps: 'maintenance',
31
+ style: 'maintenance',
32
+ }
33
+
34
+ const CONVENTIONAL_RE = /^([a-zA-Z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/
35
+
36
+ /** Parse a single commit message into conventional commit structure. */
37
+ export function parseConventionalCommit(rawMessage, sha = '') {
38
+ const full = String(rawMessage || '').trim()
39
+ const lines = full.split('\n').map((l) => l.trim())
40
+ const header = lines[0] || ''
41
+ const body = lines.slice(1).join('\n')
42
+
43
+ let type = 'other'
44
+ let scope = ''
45
+ let isBreaking = false
46
+ let subject = header
47
+ let breakingNote = ''
48
+
49
+ // Check for BREAKING CHANGE in body or footer
50
+ const breakingMatch = body.match(/BREAKING[- ]CHANGE:\s*([^\n]+)/i)
51
+ if (breakingMatch) {
52
+ isBreaking = true
53
+ breakingNote = breakingMatch[1].trim()
54
+ }
55
+
56
+ const match = header.match(CONVENTIONAL_RE)
57
+ if (match) {
58
+ type = match[1].toLowerCase()
59
+ scope = match[2] ? match[2].trim() : ''
60
+ if (match[3] === '!') isBreaking = true
61
+ subject = match[4].trim()
62
+ } else {
63
+ // Handle typical merge pull request headers e.g. "Merge pull request #123 from org/branch"
64
+ const prMatch = header.match(/^Merge pull request #(\d+)/i)
65
+ if (prMatch) {
66
+ const candidateLine = lines.slice(1).find((l) => l.trim().length > 0)
67
+ if (candidateLine) {
68
+ const subMatch = candidateLine.match(CONVENTIONAL_RE)
69
+ if (subMatch) {
70
+ type = subMatch[1].toLowerCase()
71
+ scope = subMatch[2] ? subMatch[2].trim() : ''
72
+ if (subMatch[3] === '!') isBreaking = true
73
+ subject = `${subMatch[4].trim()} (#${prMatch[1]})`
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ const categoryKey = isBreaking
80
+ ? 'breaking'
81
+ : (TYPE_TO_CATEGORY[type] || 'other')
82
+
83
+ return {
84
+ sha: sha ? sha.slice(0, 7) : '',
85
+ fullSha: sha || '',
86
+ raw: full,
87
+ header,
88
+ type,
89
+ scope,
90
+ subject,
91
+ isBreaking,
92
+ breakingNote,
93
+ category: categoryKey,
94
+ }
95
+ }
96
+
97
+ /** Calculate recommended SemVer bump from parsed commits. */
98
+ export function calculateBump(commits = []) {
99
+ if (!Array.isArray(commits) || commits.length === 0) return 'none'
100
+ if (commits.some((c) => c.isBreaking || c.category === 'breaking')) return 'major'
101
+ if (commits.some((c) => c.type === 'feat' || c.category === 'feat')) return 'minor'
102
+ return 'patch'
103
+ }
104
+
105
+ /** Bump a SemVer version string according to bump type ('major', 'minor', 'patch'). */
106
+ export function bumpVersion(version, bump) {
107
+ if (!version || bump === 'none') return version || '0.1.0'
108
+ const hasV = version.startsWith('v')
109
+ const clean = hasV ? version.slice(1) : version
110
+ const parts = clean.split('-')[0].split('.').map(Number)
111
+ let [major = 0, minor = 0, patch = 0] = parts
112
+
113
+ if (bump === 'major') {
114
+ major += 1
115
+ minor = 0
116
+ patch = 0
117
+ } else if (bump === 'minor') {
118
+ minor += 1
119
+ patch = 0
120
+ } else if (bump === 'patch') {
121
+ patch += 1
122
+ }
123
+
124
+ const res = `${major}.${minor}.${patch}`
125
+ return hasV ? `v${res}` : res
126
+ }
127
+
128
+ /** Format parsed commits into structured markdown changelog. */
129
+ export function formatChangelog({ version = '', date = '', sections = {}, compareUrl = '' } = {}) {
130
+ const headerDate = date || new Date().toISOString().slice(0, 10)
131
+ const lines = []
132
+ if (version) {
133
+ lines.push(`## [${version}] - ${headerDate}`)
134
+ } else {
135
+ lines.push(`## Changelog - ${headerDate}`)
136
+ }
137
+ lines.push('')
138
+
139
+ const sortedCategories = Object.values(CATEGORIES).sort((a, b) => a.order - b.order)
140
+
141
+ let hasEntries = false
142
+ for (const cat of sortedCategories) {
143
+ const list = sections[cat.key] || []
144
+ if (list.length === 0) continue
145
+ hasEntries = true
146
+ lines.push(`### ${cat.title}`)
147
+ for (const item of list) {
148
+ const scopePart = item.scope ? `**${item.scope}**: ` : ''
149
+ const shaPart = item.sha ? ` (${item.sha})` : ''
150
+ const breakingPrefix = (cat.key === 'breaking' && item.breakingNote) ? `*${item.breakingNote}* — ` : ''
151
+ lines.push(`- ${scopePart}${breakingPrefix}${item.subject}${shaPart}`)
152
+ }
153
+ lines.push('')
154
+ }
155
+
156
+ if (!hasEntries) {
157
+ lines.push('*No significant user-facing changes.*')
158
+ lines.push('')
159
+ }
160
+
161
+ if (compareUrl) {
162
+ lines.push(`[Full Diff](${compareUrl})`)
163
+ lines.push('')
164
+ }
165
+
166
+ return lines.join('\n').trim()
167
+ }
168
+
169
+ /** High level changelog generator. */
170
+ export async function generateChangelog(client, {
171
+ owner, repo, base, head = 'HEAD', version, date, commits: customCommits,
172
+ } = {}) {
173
+ let commitList = []
174
+ let compareUrl = ''
175
+ let previousTag = base || ''
176
+
177
+ if (Array.isArray(customCommits)) {
178
+ commitList = customCommits.map((c) => {
179
+ if (typeof c === 'string') return { message: c, sha: '' }
180
+ return { message: c.message || c.commit?.message || '', sha: c.sha || '' }
181
+ })
182
+ } else if (client && owner && repo) {
183
+ if (!previousTag) {
184
+ try {
185
+ const { data: tags } = await client.get(`/repos/${owner}/${repo}/tags`, { query: { per_page: 1 } })
186
+ if (Array.isArray(tags) && tags.length > 0) {
187
+ previousTag = tags[0].name
188
+ }
189
+ } catch {
190
+ // ignore tag fetch failure
191
+ }
192
+ }
193
+
194
+ if (previousTag) {
195
+ try {
196
+ const { data } = await client.get(`/repos/${owner}/${repo}/compare/${encodeURIComponent(previousTag)}...${encodeURIComponent(head)}`)
197
+ compareUrl = data?.html_url || ''
198
+ const rawCommits = data?.commits || []
199
+ commitList = rawCommits.map((c) => ({
200
+ message: c.commit?.message || '',
201
+ sha: c.sha || '',
202
+ author: c.author?.login || c.commit?.author?.name || '',
203
+ }))
204
+ } catch {
205
+ commitList = []
206
+ }
207
+ } else {
208
+ try {
209
+ const { data } = await client.get(`/repos/${owner}/${repo}/commits`, { query: { per_page: 50, sha: head } })
210
+ const rawCommits = Array.isArray(data) ? data : []
211
+ commitList = rawCommits.map((c) => ({
212
+ message: c.commit?.message || '',
213
+ sha: c.sha || '',
214
+ author: c.author?.login || c.commit?.author?.name || '',
215
+ }))
216
+ } catch {
217
+ commitList = []
218
+ }
219
+ }
220
+ }
221
+
222
+ const parsed = commitList.map((c) => parseConventionalCommit(c.message, c.sha))
223
+ const sections = {}
224
+ for (const catKey of Object.keys(CATEGORIES)) {
225
+ sections[catKey] = []
226
+ }
227
+ for (const item of parsed) {
228
+ const key = item.category in sections ? item.category : 'other'
229
+ sections[key].push(item)
230
+ if (item.isBreaking && item.type in sections && item.type !== 'breaking') {
231
+ sections[item.type].push(item)
232
+ }
233
+ }
234
+
235
+ const recommendedBump = calculateBump(parsed)
236
+ const nextVersion = version || (previousTag ? bumpVersion(previousTag, recommendedBump) : bumpVersion('0.1.0', recommendedBump))
237
+ const markdown = formatChangelog({ version: nextVersion, date, sections, compareUrl })
238
+
239
+ return {
240
+ version: nextVersion,
241
+ previousTag: previousTag || null,
242
+ head,
243
+ recommendedBump,
244
+ commitsCount: parsed.length,
245
+ summary: {
246
+ breaking: sections.breaking.length,
247
+ feat: sections.feat.length,
248
+ fix: sections.fix.length,
249
+ perf: sections.perf.length,
250
+ refactor: sections.refactor.length,
251
+ docs: sections.docs.length,
252
+ test: sections.test.length,
253
+ maintenance: sections.maintenance.length,
254
+ other: sections.other.length,
255
+ total: parsed.length,
256
+ },
257
+ sections,
258
+ markdown,
259
+ commits: parsed,
260
+ }
261
+ }
@@ -0,0 +1,238 @@
1
+ // Community health files scaffolding tools.
2
+ // Standardized templates for GoodAndReady open source plugins and repositories.
3
+
4
+ import { pushFiles } from './write-files.js'
5
+
6
+ export const TEMPLATE_KEYS = [
7
+ 'bug_report',
8
+ 'feature_request',
9
+ 'security_advisory',
10
+ 'pull_request',
11
+ 'security',
12
+ 'contributing',
13
+ 'code_of_conduct',
14
+ ]
15
+
16
+ /** Generate file mapping for requested community templates. */
17
+ export function getCommunityTemplates({
18
+ projectName = 'Project',
19
+ contactEmail = 'security@goodandready.app',
20
+ defaultBranch = 'main',
21
+ supportVersion = '>= 0.6.0',
22
+ } = {}) {
23
+ const templates = {}
24
+
25
+ templates['.github/ISSUE_TEMPLATE/bug_report.md'] = `---
26
+ name: Bug Report
27
+ about: Create a report to help us improve
28
+ title: 'fix: '
29
+ labels: ['bug']
30
+ assignees: ''
31
+ ---
32
+
33
+ ### 1. Context & Description
34
+ <!-- A clear and concise description of what the bug is. -->
35
+
36
+ ### 2. Steps to Reproduce
37
+ 1. Go to '...'
38
+ 2. Click on '....'
39
+ 3. See error
40
+
41
+ ### 3. Expected Behavior
42
+ <!-- What you expected to happen. -->
43
+
44
+ ### 4. Actual Behavior / Logs
45
+ <!-- Screenshots or error logs if applicable. -->
46
+
47
+ ### 5. Environment
48
+ - OS: [e.g. Linux / Windows]
49
+ - Node version: [e.g. 22.x]
50
+ - Plugin version: [e.g. ${supportVersion}]
51
+ `
52
+
53
+ templates['.github/ISSUE_TEMPLATE/feature_request.md'] = `---
54
+ name: Feature Request
55
+ about: Suggest an idea or enhancement
56
+ title: 'feat: '
57
+ labels: ['enhancement']
58
+ assignees: ''
59
+ ---
60
+
61
+ ### 1. Problem Statement
62
+ <!-- Is your feature request related to a problem? Please describe. -->
63
+
64
+ ### 2. Proposed Solution
65
+ <!-- Describe the solution you'd like. -->
66
+
67
+ ### 3. Acceptance Criteria
68
+ - [ ] Functional requirements met
69
+ - [ ] Automated tests added
70
+ - [ ] Documentation updated
71
+
72
+ ### 4. Alternatives Considered
73
+ <!-- Any alternatives or workaround you have considered. -->
74
+ `
75
+
76
+ templates['.github/ISSUE_TEMPLATE/security_advisory.md'] = `---
77
+ name: Security Vulnerability
78
+ about: Privately report a security issue
79
+ title: 'sec: '
80
+ labels: ['security']
81
+ assignees: ''
82
+ ---
83
+
84
+ > [!IMPORTANT]
85
+ > If this is a critical zero-day security vulnerability, please contact ${contactEmail} directly instead of opening a public issue.
86
+
87
+ ### 1. Summary
88
+ ### 2. Severity & Impact
89
+ ### 3. Steps / Proof of Concept
90
+ ### 4. Remediation / Suggested Patch
91
+ `
92
+
93
+ templates['.github/PULL_REQUEST_TEMPLATE.md'] = `## 1. Context & Problem
94
+ <!-- Reference issue or explain the problem solved. -->
95
+
96
+ ## 2. Proposed Changes
97
+ <!-- Detailed explanation of architectural and code modifications. -->
98
+
99
+ ## 3. Verification & Testing
100
+ - [ ] Unit tests added / updated
101
+ - [ ] All tests passing (\`npm test\`)
102
+ - [ ] No regression on existing invariants
103
+
104
+ ## 4. Documentation & Contracts
105
+ - [ ] Documentation updated (README / API docs)
106
+ - [ ] Conventional Commits used for automated release notes
107
+
108
+ ## 5. Review Checklist
109
+ - [ ] Code style and lint verified
110
+ - [ ] Zero hardcoded secrets or credentials
111
+ `
112
+
113
+ templates['SECURITY.md'] = `# Security Policy
114
+
115
+ ## Supported Versions
116
+
117
+ | Version | Supported |
118
+ | ------- | ------------------ |
119
+ | ${supportVersion} | :white_check_mark: |
120
+ | < 0.6.0 | :x: |
121
+
122
+ ## Reporting a Vulnerability
123
+
124
+ Please report security vulnerabilities directly to \`${contactEmail}\`.
125
+ All reports will be acknowledged within 48 hours, and an advisory/patch roadmap provided within 7 days.
126
+ `
127
+
128
+ templates['CONTRIBUTING.md'] = `# Contributing to ${projectName}
129
+
130
+ Thank you for your interest in contributing!
131
+
132
+ ## Development Workflow
133
+ 1. Fork or branch from \`${defaultBranch}\`.
134
+ 2. Install dependencies: \`pnpm install\` or \`npm install\`.
135
+ 3. Run tests: \`npm test\`.
136
+
137
+ ## Conventional Commits
138
+ We follow the Conventional Commits specification:
139
+ - \`feat:\` new feature (triggers minor version bump)
140
+ - \`fix:\` bug fix (triggers patch bump)
141
+ - \`perf:\` performance improvement
142
+ - \`refactor:\` code refactoring
143
+ - \`docs:\` documentation updates
144
+ - \`chore:\` maintenance and tooling
145
+ - \`BREAKING CHANGE:\` incompatible API change (triggers major bump)
146
+
147
+ ## Pull Request Process
148
+ - Fill out the 5-section PR template.
149
+ - Ensure all automated checks pass.
150
+ `
151
+
152
+ templates['CODE_OF_CONDUCT.md'] = `# Contributor Covenant Code of Conduct
153
+
154
+ ## Our Pledge
155
+ We as members, contributors, and leaders pledge to make participation in our
156
+ community a harassment-free experience for everyone, regardless of age, body
157
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
158
+ identity and expression, level of experience, education, socio-economic status,
159
+ nationality, personal appearance, race, caste, color, religion, or sexual
160
+ identity and orientation.
161
+
162
+ ## Enforcement & Contact
163
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
164
+ reported to the community leadership at \`${contactEmail}\`.
165
+ `
166
+
167
+ return templates
168
+ }
169
+
170
+ /** Map friendly template keys to path patterns */
171
+ const KEY_TO_PATH_PATTERN = {
172
+ bug_report: '.github/ISSUE_TEMPLATE/bug_report.md',
173
+ feature_request: '.github/ISSUE_TEMPLATE/feature_request.md',
174
+ security_advisory: '.github/ISSUE_TEMPLATE/security_advisory.md',
175
+ issue_templates: '.github/ISSUE_TEMPLATE/',
176
+ pull_request: '.github/PULL_REQUEST_TEMPLATE.md',
177
+ pr_template: '.github/PULL_REQUEST_TEMPLATE.md',
178
+ security: 'SECURITY.md',
179
+ contributing: 'CONTRIBUTING.md',
180
+ code_of_conduct: 'CODE_OF_CONDUCT.md',
181
+ }
182
+
183
+ /** Scaffold community files into a repo branch or preview via dryRun. */
184
+ export async function scaffoldCommunityFiles(client, {
185
+ owner, repo, branch, templates = 'all', options = {}, dryRun = false, message,
186
+ } = {}) {
187
+ const allTemplates = getCommunityTemplates({
188
+ projectName: options.projectName || repo || 'Project',
189
+ contactEmail: options.contactEmail || 'security@goodandready.app',
190
+ defaultBranch: branch || 'main',
191
+ supportVersion: options.supportVersion || '>= 0.6.0',
192
+ })
193
+
194
+ let selectedPaths = Object.keys(allTemplates)
195
+ if (Array.isArray(templates) && !templates.includes('all')) {
196
+ const desiredPatterns = templates.map((t) => KEY_TO_PATH_PATTERN[t] || t)
197
+ selectedPaths = selectedPaths.filter((filePath) =>
198
+ desiredPatterns.some((pattern) => filePath === pattern || filePath.startsWith(pattern))
199
+ )
200
+ }
201
+
202
+ const files = selectedPaths.map((filePath) => ({
203
+ path: filePath,
204
+ content: allTemplates[filePath],
205
+ }))
206
+
207
+ if (dryRun) {
208
+ return {
209
+ dryRun: true,
210
+ repository: `${owner}/${repo}`,
211
+ branch: branch || 'main',
212
+ count: files.length,
213
+ files,
214
+ }
215
+ }
216
+
217
+ if (!client || !owner || !repo) {
218
+ throw new Error('client, owner and repo are required to commit files')
219
+ }
220
+
221
+ const commitMessage = message || 'chore: scaffold GoodAndReady community health files'
222
+ const pushed = await pushFiles(client, {
223
+ owner,
224
+ repo,
225
+ branch: branch || 'main',
226
+ message: commitMessage,
227
+ files,
228
+ })
229
+
230
+ return {
231
+ success: true,
232
+ repository: `${owner}/${repo}`,
233
+ branch: pushed.branch,
234
+ commit: pushed.commit,
235
+ count: files.length,
236
+ files: files.map((f) => f.path),
237
+ }
238
+ }