github-weekly-reporter 0.1.0 → 0.2.1

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.
Files changed (176) hide show
  1. package/README.md +86 -115
  2. package/dist/cli/commands/deploy.d.ts.map +1 -1
  3. package/dist/cli/commands/deploy.js +9 -3
  4. package/dist/cli/commands/deploy.js.map +1 -1
  5. package/dist/cli/commands/fetch.d.ts +3 -0
  6. package/dist/cli/commands/fetch.d.ts.map +1 -0
  7. package/dist/cli/commands/fetch.js +148 -0
  8. package/dist/cli/commands/fetch.js.map +1 -0
  9. package/dist/cli/commands/generate.d.ts.map +1 -1
  10. package/dist/cli/commands/generate.js +59 -80
  11. package/dist/cli/commands/generate.js.map +1 -1
  12. package/dist/cli/commands/render.d.ts +3 -0
  13. package/dist/cli/commands/render.d.ts.map +1 -0
  14. package/dist/cli/commands/render.js +172 -0
  15. package/dist/cli/commands/render.js.map +1 -0
  16. package/dist/cli/commands/setup.d.ts +3 -0
  17. package/dist/cli/commands/setup.d.ts.map +1 -0
  18. package/dist/cli/commands/setup.js +709 -0
  19. package/dist/cli/commands/setup.js.map +1 -0
  20. package/dist/cli/index.js +6 -0
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/collector/clean-body.d.ts +2 -0
  23. package/dist/collector/clean-body.d.ts.map +1 -0
  24. package/dist/collector/clean-body.js +27 -0
  25. package/dist/collector/clean-body.js.map +1 -0
  26. package/dist/collector/date-range.d.ts +3 -2
  27. package/dist/collector/date-range.d.ts.map +1 -1
  28. package/dist/collector/date-range.js +125 -5
  29. package/dist/collector/date-range.js.map +1 -1
  30. package/dist/collector/fetch-contributions.d.ts +11 -1
  31. package/dist/collector/fetch-contributions.d.ts.map +1 -1
  32. package/dist/collector/fetch-contributions.js +15 -3
  33. package/dist/collector/fetch-contributions.js.map +1 -1
  34. package/dist/collector/fetch-events.d.ts +6 -0
  35. package/dist/collector/fetch-events.d.ts.map +1 -0
  36. package/dist/collector/fetch-events.js +106 -0
  37. package/dist/collector/fetch-events.js.map +1 -0
  38. package/dist/collector/fetch-repo-prs.d.ts +7 -0
  39. package/dist/collector/fetch-repo-prs.d.ts.map +1 -0
  40. package/dist/collector/fetch-repo-prs.js +62 -0
  41. package/dist/collector/fetch-repo-prs.js.map +1 -0
  42. package/dist/collector/queries.d.ts +1 -4
  43. package/dist/collector/queries.d.ts.map +1 -1
  44. package/dist/collector/queries.js +7 -59
  45. package/dist/collector/queries.js.map +1 -1
  46. package/dist/deployer/index-page.d.ts +22 -2
  47. package/dist/deployer/index-page.d.ts.map +1 -1
  48. package/dist/deployer/index-page.js +424 -33
  49. package/dist/deployer/index-page.js.map +1 -1
  50. package/dist/deployer/week.d.ts +1 -1
  51. package/dist/deployer/week.d.ts.map +1 -1
  52. package/dist/deployer/week.js +17 -6
  53. package/dist/deployer/week.js.map +1 -1
  54. package/dist/i18n/fonts.d.ts +8 -0
  55. package/dist/i18n/fonts.d.ts.map +1 -0
  56. package/dist/i18n/fonts.js +44 -0
  57. package/dist/i18n/fonts.js.map +1 -0
  58. package/dist/i18n/index.d.ts +28 -0
  59. package/dist/i18n/index.d.ts.map +1 -0
  60. package/dist/i18n/index.js +259 -0
  61. package/dist/i18n/index.js.map +1 -0
  62. package/dist/index.d.ts +2 -3
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +1 -2
  65. package/dist/index.js.map +1 -1
  66. package/dist/llm/index.d.ts +2 -1
  67. package/dist/llm/index.d.ts.map +1 -1
  68. package/dist/llm/index.js +77 -5
  69. package/dist/llm/index.js.map +1 -1
  70. package/dist/llm/preprocess.d.ts +3 -0
  71. package/dist/llm/preprocess.d.ts.map +1 -0
  72. package/dist/llm/preprocess.js +92 -0
  73. package/dist/llm/preprocess.js.map +1 -0
  74. package/dist/llm/prompt.d.ts.map +1 -1
  75. package/dist/llm/prompt.js +109 -29
  76. package/dist/llm/prompt.js.map +1 -1
  77. package/dist/llm/providers/anthropic.js +1 -1
  78. package/dist/llm/providers/anthropic.js.map +1 -1
  79. package/dist/llm/providers/grok.d.ts +3 -0
  80. package/dist/llm/providers/grok.d.ts.map +1 -0
  81. package/dist/llm/providers/grok.js +18 -0
  82. package/dist/llm/providers/grok.js.map +1 -0
  83. package/dist/llm/providers/groq.d.ts +3 -0
  84. package/dist/llm/providers/groq.d.ts.map +1 -0
  85. package/dist/llm/providers/groq.js +18 -0
  86. package/dist/llm/providers/groq.js.map +1 -0
  87. package/dist/llm/providers/openai.js +1 -1
  88. package/dist/llm/providers/openai.js.map +1 -1
  89. package/dist/llm/providers/openrouter.d.ts +3 -0
  90. package/dist/llm/providers/openrouter.d.ts.map +1 -0
  91. package/dist/llm/providers/openrouter.js +18 -0
  92. package/dist/llm/providers/openrouter.js.map +1 -0
  93. package/dist/llm/types.d.ts +5 -2
  94. package/dist/llm/types.d.ts.map +1 -1
  95. package/dist/renderer/helpers.d.ts +6 -1
  96. package/dist/renderer/helpers.d.ts.map +1 -1
  97. package/dist/renderer/helpers.js +54 -17
  98. package/dist/renderer/helpers.js.map +1 -1
  99. package/dist/renderer/index.d.ts +11 -2
  100. package/dist/renderer/index.d.ts.map +1 -1
  101. package/dist/renderer/index.js +41 -19
  102. package/dist/renderer/index.js.map +1 -1
  103. package/dist/renderer/og-image.d.ts +15 -0
  104. package/dist/renderer/og-image.d.ts.map +1 -0
  105. package/dist/renderer/og-image.js +152 -0
  106. package/dist/renderer/og-image.js.map +1 -0
  107. package/dist/renderer/themes.d.ts +2 -17
  108. package/dist/renderer/themes.d.ts.map +1 -1
  109. package/dist/renderer/themes.js +371 -147
  110. package/dist/renderer/themes.js.map +1 -1
  111. package/dist/types.d.ts +103 -11
  112. package/dist/types.d.ts.map +1 -1
  113. package/package.json +10 -4
  114. package/src/renderer/templates/partials/footer.hbs +3 -1
  115. package/src/renderer/templates/partials/header.hbs +10 -7
  116. package/src/renderer/templates/partials/highlights.hbs +24 -0
  117. package/src/renderer/templates/partials/overview.hbs +3 -0
  118. package/src/renderer/templates/partials/summaries.hbs +73 -0
  119. package/src/renderer/templates/report.hbs +100 -15
  120. package/dist/cli/config.d.ts +0 -11
  121. package/dist/cli/config.d.ts.map +0 -1
  122. package/dist/cli/config.js +0 -16
  123. package/dist/cli/config.js.map +0 -1
  124. package/dist/cli/config.test.d.ts +0 -2
  125. package/dist/cli/config.test.d.ts.map +0 -1
  126. package/dist/cli/config.test.js +0 -32
  127. package/dist/cli/config.test.js.map +0 -1
  128. package/dist/collector/aggregate.test.d.ts +0 -2
  129. package/dist/collector/aggregate.test.d.ts.map +0 -1
  130. package/dist/collector/aggregate.test.js +0 -88
  131. package/dist/collector/aggregate.test.js.map +0 -1
  132. package/dist/collector/date-range.test.d.ts +0 -2
  133. package/dist/collector/date-range.test.d.ts.map +0 -1
  134. package/dist/collector/date-range.test.js +0 -25
  135. package/dist/collector/date-range.test.js.map +0 -1
  136. package/dist/collector/fetch-issues.d.ts +0 -5
  137. package/dist/collector/fetch-issues.d.ts.map +0 -1
  138. package/dist/collector/fetch-issues.js +0 -31
  139. package/dist/collector/fetch-issues.js.map +0 -1
  140. package/dist/collector/fetch-languages.d.ts +0 -4
  141. package/dist/collector/fetch-languages.d.ts.map +0 -1
  142. package/dist/collector/fetch-languages.js +0 -42
  143. package/dist/collector/fetch-languages.js.map +0 -1
  144. package/dist/collector/fetch-pull-requests.d.ts +0 -5
  145. package/dist/collector/fetch-pull-requests.d.ts.map +0 -1
  146. package/dist/collector/fetch-pull-requests.js +0 -31
  147. package/dist/collector/fetch-pull-requests.js.map +0 -1
  148. package/dist/collector/index.d.ts +0 -3
  149. package/dist/collector/index.d.ts.map +0 -1
  150. package/dist/collector/index.js +0 -50
  151. package/dist/collector/index.js.map +0 -1
  152. package/dist/deployer/index-page.test.d.ts +0 -2
  153. package/dist/deployer/index-page.test.d.ts.map +0 -1
  154. package/dist/deployer/index-page.test.js +0 -29
  155. package/dist/deployer/index-page.test.js.map +0 -1
  156. package/dist/deployer/week.test.d.ts +0 -2
  157. package/dist/deployer/week.test.d.ts.map +0 -1
  158. package/dist/deployer/week.test.js +0 -21
  159. package/dist/deployer/week.test.js.map +0 -1
  160. package/dist/llm/llm.test.d.ts +0 -2
  161. package/dist/llm/llm.test.d.ts.map +0 -1
  162. package/dist/llm/llm.test.js +0 -24
  163. package/dist/llm/llm.test.js.map +0 -1
  164. package/dist/llm/prompt.test.d.ts +0 -2
  165. package/dist/llm/prompt.test.d.ts.map +0 -1
  166. package/dist/llm/prompt.test.js +0 -48
  167. package/dist/llm/prompt.test.js.map +0 -1
  168. package/dist/renderer/renderer.test.d.ts +0 -2
  169. package/dist/renderer/renderer.test.d.ts.map +0 -1
  170. package/dist/renderer/renderer.test.js +0 -111
  171. package/dist/renderer/renderer.test.js.map +0 -1
  172. package/src/renderer/templates/partials/heatmap.hbs +0 -11
  173. package/src/renderer/templates/partials/languages.hbs +0 -19
  174. package/src/renderer/templates/partials/narrative.hbs +0 -10
  175. package/src/renderer/templates/partials/repositories.hbs +0 -25
  176. package/src/renderer/templates/partials/stats.hbs +0 -8
@@ -0,0 +1,709 @@
1
+ // setup command: interactive one-command setup for GitHub Weekly Reporter
2
+ import { input, select, password, confirm } from "@inquirer/prompts";
3
+ const ghHeaders = (token) => ({
4
+ Authorization: `Bearer ${token}`,
5
+ Accept: "application/vnd.github+json",
6
+ "X-GitHub-Api-Version": "2022-11-28",
7
+ "Content-Type": "application/json",
8
+ });
9
+ const ghFetch = async (token, method, path, body) => fetch(`https://api.github.com${path}`, {
10
+ method,
11
+ headers: ghHeaders(token),
12
+ ...(body ? { body: JSON.stringify(body) } : {}),
13
+ });
14
+ const ghGet = (token, path) => ghFetch(token, "GET", path);
15
+ const ghPost = (token, path, body) => ghFetch(token, "POST", path, body);
16
+ const ghPut = (token, path, body) => ghFetch(token, "PUT", path, body);
17
+ // ── Token validation ─────────────────────────────────────────
18
+ const validateToken = async (token) => {
19
+ const res = await ghGet(token, "/user");
20
+ if (res.status === 401) {
21
+ throw new Error("Invalid or expired token.\n\n" +
22
+ " Create a token at: https://github.com/settings/tokens\n\n" +
23
+ " Classic PAT scopes needed: repo, workflow\n" +
24
+ " Fine-grained PAT:\n" +
25
+ " Repository access: All repositories\n" +
26
+ " Permissions: Administration, Contents, Actions,\n" +
27
+ " Secrets, Pages, Workflows (all Read & Write)");
28
+ }
29
+ if (!res.ok)
30
+ throw new Error(`GitHub API error: ${res.status}`);
31
+ const { login } = (await res.json());
32
+ const scopeHeader = res.headers.get("x-oauth-scopes");
33
+ // Fine-grained tokens do not return x-oauth-scopes header.
34
+ // We cannot validate permissions upfront, so we validate lazily
35
+ // when each API call is made and provide clear error messages.
36
+ if (scopeHeader === null) {
37
+ return { login, tokenType: "fine-grained" };
38
+ }
39
+ // Classic PAT: validate scopes
40
+ const scopes = scopeHeader.split(",").map((s) => s.trim());
41
+ const missing = ["repo", "workflow"].filter((s) => !scopes.includes(s));
42
+ if (missing.length > 0) {
43
+ throw new Error(`Token is missing required scopes: ${missing.join(", ")}\n\n` +
44
+ " Create a new token at: https://github.com/settings/tokens/new?scopes=repo,workflow\n" +
45
+ " Required scopes: repo, workflow");
46
+ }
47
+ return { login, tokenType: "classic" };
48
+ };
49
+ // ── Secret encryption (sealed box via tweetsodium) ───────────
50
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
51
+ const setRepoSecret = async (token, repo, name, value) => {
52
+ const tweetsodium = await import("tweetsodium");
53
+ const seal = tweetsodium.default.seal;
54
+ // Retry up to 3 times with backoff (new repos may need time to propagate)
55
+ for (let attempt = 0; attempt < 3; attempt++) {
56
+ const keyRes = await ghGet(token, `/repos/${repo}/actions/secrets/public-key`);
57
+ if (!keyRes.ok) {
58
+ if (attempt < 2) {
59
+ await sleep(3000 * (attempt + 1));
60
+ continue;
61
+ }
62
+ return false;
63
+ }
64
+ const { key, key_id } = (await keyRes.json());
65
+ const keyBytes = Uint8Array.from(atob(key), (c) => c.charCodeAt(0));
66
+ const encrypted = seal(new TextEncoder().encode(value), keyBytes);
67
+ const encryptedB64 = btoa(String.fromCharCode(...encrypted));
68
+ const res = await ghPut(token, `/repos/${repo}/actions/secrets/${name}`, {
69
+ encrypted_value: encryptedB64,
70
+ key_id,
71
+ });
72
+ if (res.ok)
73
+ return true;
74
+ const body = await res.text().catch(() => "");
75
+ console.log(` Attempt ${attempt + 1}/3 failed: ${res.status} ${body.slice(0, 200)}`);
76
+ if (attempt < 2) {
77
+ await sleep(3000 * (attempt + 1));
78
+ continue;
79
+ }
80
+ return false;
81
+ }
82
+ return false;
83
+ };
84
+ // ── Constants ────────────────────────────────────────────────
85
+ const LLM_SECRET_NAMES = {
86
+ openai: "OPENAI_API_KEY",
87
+ anthropic: "ANTHROPIC_API_KEY",
88
+ gemini: "GEMINI_API_KEY",
89
+ openrouter: "OPENROUTER_API_KEY",
90
+ groq: "GROQ_API_KEY",
91
+ grok: "GROK_API_KEY",
92
+ };
93
+ const LLM_API_KEY_INPUT_NAMES = {
94
+ openai: "openai-api-key",
95
+ anthropic: "anthropic-api-key",
96
+ gemini: "gemini-api-key",
97
+ openrouter: "openrouter-api-key",
98
+ groq: "groq-api-key",
99
+ grok: "grok-api-key",
100
+ };
101
+ const TIMEZONE_CHOICES = [
102
+ { name: "UTC", value: "UTC" },
103
+ { name: "US/Pacific (Los Angeles)", value: "America/Los_Angeles" },
104
+ { name: "US/Mountain (Denver)", value: "America/Denver" },
105
+ { name: "US/Central (Chicago)", value: "America/Chicago" },
106
+ { name: "US/Eastern (New York)", value: "America/New_York" },
107
+ { name: "Europe/London", value: "Europe/London" },
108
+ { name: "Europe/Paris", value: "Europe/Paris" },
109
+ { name: "Europe/Berlin", value: "Europe/Berlin" },
110
+ { name: "Europe/Moscow", value: "Europe/Moscow" },
111
+ { name: "Asia/Dubai", value: "Asia/Dubai" },
112
+ { name: "Asia/Kolkata (India)", value: "Asia/Kolkata" },
113
+ { name: "Asia/Bangkok", value: "Asia/Bangkok" },
114
+ { name: "Asia/Shanghai (China)", value: "Asia/Shanghai" },
115
+ { name: "Asia/Tokyo (Japan)", value: "Asia/Tokyo" },
116
+ { name: "Asia/Seoul (Korea)", value: "Asia/Seoul" },
117
+ { name: "Australia/Sydney", value: "Australia/Sydney" },
118
+ { name: "Pacific/Auckland (New Zealand)", value: "Pacific/Auckland" },
119
+ { name: "America/Sao_Paulo (Brazil)", value: "America/Sao_Paulo" },
120
+ ];
121
+ const LANGUAGE_CHOICES = [
122
+ { name: "English", value: "en" },
123
+ { name: "Japanese (日本語)", value: "ja" },
124
+ { name: "Chinese Simplified (简体中文)", value: "zh-CN" },
125
+ { name: "Chinese Traditional (繁體中文)", value: "zh-TW" },
126
+ { name: "Korean (한국어)", value: "ko" },
127
+ { name: "Spanish (Español)", value: "es" },
128
+ { name: "French (Français)", value: "fr" },
129
+ { name: "German (Deutsch)", value: "de" },
130
+ { name: "Portuguese (Português)", value: "pt" },
131
+ { name: "Russian (Русский)", value: "ru" },
132
+ ];
133
+ const MODEL_LIST_URLS = {
134
+ groq: "https://console.groq.com/docs/models",
135
+ openrouter: "https://openrouter.ai/models",
136
+ openai: "https://platform.openai.com/docs/models",
137
+ anthropic: "https://docs.anthropic.com/en/docs/about-claude/models",
138
+ gemini: "https://ai.google.dev/gemini-api/docs/models",
139
+ grok: "https://docs.x.ai/docs/models",
140
+ };
141
+ // Validate model by making a minimal API call
142
+ const validateModel = async (provider, apiKey, model) => {
143
+ const openaiCompatible = (baseURL) => fetch(`${baseURL}/chat/completions`, {
144
+ method: "POST",
145
+ headers: {
146
+ Authorization: `Bearer ${apiKey}`,
147
+ "Content-Type": "application/json",
148
+ },
149
+ body: JSON.stringify({
150
+ model,
151
+ messages: [{ role: "user", content: "hi" }],
152
+ max_tokens: 1,
153
+ }),
154
+ });
155
+ try {
156
+ let res;
157
+ switch (provider) {
158
+ case "openai":
159
+ res = await openaiCompatible("https://api.openai.com/v1");
160
+ break;
161
+ case "groq":
162
+ res = await openaiCompatible("https://api.groq.com/openai/v1");
163
+ break;
164
+ case "openrouter":
165
+ res = await openaiCompatible("https://openrouter.ai/api/v1");
166
+ break;
167
+ case "grok":
168
+ res = await openaiCompatible("https://api.x.ai/v1");
169
+ break;
170
+ case "anthropic":
171
+ res = await fetch("https://api.anthropic.com/v1/messages", {
172
+ method: "POST",
173
+ headers: {
174
+ "x-api-key": apiKey,
175
+ "anthropic-version": "2023-06-01",
176
+ "Content-Type": "application/json",
177
+ },
178
+ body: JSON.stringify({
179
+ model,
180
+ messages: [{ role: "user", content: "hi" }],
181
+ max_tokens: 1,
182
+ }),
183
+ });
184
+ break;
185
+ case "gemini":
186
+ res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
187
+ method: "POST",
188
+ headers: { "Content-Type": "application/json" },
189
+ body: JSON.stringify({
190
+ contents: [{ parts: [{ text: "hi" }] }],
191
+ generationConfig: { maxOutputTokens: 1 },
192
+ }),
193
+ });
194
+ break;
195
+ default:
196
+ return { valid: true };
197
+ }
198
+ if (res.ok)
199
+ return { valid: true };
200
+ const body = await res.text();
201
+ // 404 or "model not found" means wrong model name
202
+ if (res.status === 404 || body.toLowerCase().includes("model")) {
203
+ return { valid: false, error: `Model "${model}" not found (${res.status})` };
204
+ }
205
+ // Rate limit or other non-model errors are fine (model exists)
206
+ if (res.status === 429)
207
+ return { valid: true };
208
+ return { valid: false, error: `API error: ${res.status} ${body.slice(0, 200)}` };
209
+ }
210
+ catch (e) {
211
+ return { valid: false, error: `Connection error: ${e instanceof Error ? e.message : String(e)}` };
212
+ }
213
+ };
214
+ // ── Cron calculation from timezone ───────────────────────────
215
+ // Daily fetch should run at midnight in the user's timezone.
216
+ // GitHub Actions cron is always UTC, so we calculate the offset.
217
+ const midnightCronUTC = (timezone) => {
218
+ const now = new Date();
219
+ const utcMidnight = new Date(now.toLocaleString("en-US", { timeZone: "UTC" }));
220
+ const localMidnight = new Date(now.toLocaleString("en-US", { timeZone: timezone }));
221
+ const offsetMinutes = Math.round((utcMidnight.getTime() - localMidnight.getTime()) / 60000);
222
+ const utcHour = ((offsetMinutes / 60 + 24) % 24) | 0;
223
+ const utcMinute = ((offsetMinutes % 60) + 60) % 60;
224
+ return `${utcMinute} ${utcHour} * * *`;
225
+ };
226
+ const buildDailyWorkflow = (opts) => {
227
+ const cron = midnightCronUTC(opts.timezone);
228
+ return `# Generated by: github-weekly-reporter setup
229
+ # Docs: https://github.com/deariary/github-weekly-reporter
230
+
231
+ name: Daily Fetch
232
+
233
+ on:
234
+ schedule:
235
+ - cron: '${cron}' # midnight ${opts.timezone}
236
+ workflow_dispatch:
237
+
238
+ permissions:
239
+ contents: write
240
+
241
+ jobs:
242
+ daily:
243
+ runs-on: ubuntu-latest
244
+ steps:
245
+ - uses: actions/checkout@v4
246
+
247
+ - uses: deariary/github-weekly-reporter@main
248
+ with:
249
+ github-token: \${{ secrets.GH_PAT }}
250
+ username: '${opts.username}'
251
+ mode: 'daily'
252
+ language: '${opts.language}'
253
+ timezone: '${opts.timezone}'
254
+ `;
255
+ };
256
+ const buildWeeklyWorkflow = (opts) => {
257
+ // Run 1 hour after daily fetch, Monday only
258
+ const dailyCron = midnightCronUTC(opts.timezone);
259
+ const [minute, hour] = dailyCron.split(" ").map(Number);
260
+ const weeklyHour = (hour + 1) % 24;
261
+ const weeklyCron = `${minute} ${weeklyHour} * * 1`;
262
+ const llmInputs = opts.llmProvider && opts.llmModel && opts.llmSecretName
263
+ ? ` llm-provider: '${opts.llmProvider}'
264
+ llm-model: '${opts.llmModel}'
265
+ ${LLM_API_KEY_INPUT_NAMES[opts.llmProvider]}: \${{ secrets.${opts.llmSecretName} }}`
266
+ : "";
267
+ return `# Generated by: github-weekly-reporter setup
268
+ # Docs: https://github.com/deariary/github-weekly-reporter
269
+
270
+ name: Weekly Report
271
+
272
+ on:
273
+ schedule:
274
+ - cron: '${weeklyCron}' # Monday, 1 hour after daily fetch (${opts.timezone})
275
+ workflow_dispatch:
276
+
277
+ permissions:
278
+ contents: write
279
+ pages: write
280
+
281
+ env:
282
+ SITE_TITLE: '${opts.siteTitle}'
283
+
284
+ jobs:
285
+ weekly:
286
+ runs-on: ubuntu-latest
287
+ steps:
288
+ - uses: actions/checkout@v4
289
+
290
+ - uses: deariary/github-weekly-reporter@main
291
+ with:
292
+ github-token: \${{ secrets.GH_PAT }}
293
+ username: '${opts.username}'
294
+ mode: 'weekly'
295
+ language: '${opts.language}'
296
+ timezone: '${opts.timezone}'
297
+ ${llmInputs}
298
+ `;
299
+ };
300
+ // ── README template ──────────────────────────────────────────
301
+ const buildReadme = (opts) => `# ${opts.siteTitle}
302
+
303
+ Weekly GitHub activity reports for [@${opts.username}](https://github.com/${opts.username}), powered by [github-weekly-reporter](https://github.com/deariary/github-weekly-reporter).
304
+
305
+ ## Live Reports
306
+
307
+ ${opts.pagesUrl}
308
+
309
+ ## How It Works
310
+
311
+ 1. **Daily** (automatic): A scheduled workflow collects your GitHub events at midnight (${opts.timezone}).
312
+ 2. **Weekly** (manual): Trigger the workflow with \`mode: weekly\` from the [Actions tab](https://github.com/${opts.repo}/actions) to generate a full report${opts.llmProvider ? " with AI narrative" : ""}.
313
+ 3. The report is deployed to GitHub Pages automatically.
314
+
315
+ ## Configuration
316
+
317
+ Edit \`.github/workflows/weekly-report.yml\` to change:
318
+
319
+ | Setting | Current | Description |
320
+ |---------|---------|-------------|
321
+ | \`username\` | \`${opts.username}\` | GitHub user to report on |
322
+ | \`language\` | \`${opts.language}\` | Report language (en, ja, zh-CN, zh-TW, ko, es, fr, de, pt, ru) |
323
+ | \`timezone\` | \`${opts.timezone}\` | IANA timezone for date calculations |
324
+ | \`SITE_TITLE\` | \`${opts.siteTitle}\` | Site title in the header and hero |
325
+ ${opts.llmProvider ? `| \`llm-provider\` | \`${opts.llmProvider}\` | LLM provider for AI narrative |\n| \`llm-model\` | \`${opts.llmModel}\` | Model name |\n` : ""}
326
+ ## Base URL
327
+
328
+ The report's canonical URL, OG images, and sitemap are generated using \`BASE_URL\`.
329
+ By default this is derived automatically from the repository name:
330
+
331
+ \`\`\`
332
+ https://${opts.repo.split("/")[0]}.github.io/${opts.repo.split("/")[1]}
333
+ \`\`\`
334
+
335
+ If you use a **custom domain**, add \`BASE_URL\` to the workflow env:
336
+
337
+ \`\`\`yaml
338
+ env:
339
+ SITE_TITLE: '${opts.siteTitle}'
340
+ BASE_URL: 'https://your-custom-domain.com'
341
+ \`\`\`
342
+
343
+ Then configure the custom domain in **Settings > Pages > Custom domain**.
344
+
345
+ ## Changing the LLM API Key
346
+
347
+ 1. Go to **Settings > Secrets and variables > Actions**
348
+ 2. Update the \`${opts.llmProvider ? LLM_SECRET_NAMES[opts.llmProvider] : "LLM_API_KEY"}\` secret
349
+
350
+ ## Manual Report Generation
351
+
352
+ Go to [Actions](https://github.com/${opts.repo}/actions), click **Weekly Report**, then **Run workflow** with \`mode: weekly\`.
353
+ `;
354
+ const collectInputs = async (cliRepo) => {
355
+ console.log("\n GitHub Weekly Reporter - Interactive Setup\n");
356
+ console.log(" This will create a repository, add a workflow, and configure");
357
+ console.log(" everything you need for automatic weekly reports.\n");
358
+ // 1. Token
359
+ console.log(" A personal access token (PAT) is required.\n");
360
+ console.log(" Classic PAT (https://github.com/settings/tokens/new?scopes=repo,workflow):");
361
+ console.log(" Scopes: repo, workflow\n");
362
+ console.log(" Fine-grained PAT (https://github.com/settings/personal-access-tokens/new):");
363
+ console.log(" Repository access: All repositories");
364
+ console.log(" Permissions: Administration, Contents, Actions,");
365
+ console.log(" Secrets, Pages, Workflows (all Read & Write)\n");
366
+ const token = await password({
367
+ message: "GitHub personal access token:",
368
+ validate: (v) => (v.length > 0 ? true : "Token is required"),
369
+ });
370
+ console.log("\n Validating token...");
371
+ const { login } = await validateToken(token);
372
+ console.log(` Authenticated as ${login}\n`);
373
+ // 2. Username
374
+ const username = await input({
375
+ message: "GitHub username to report on:",
376
+ default: login,
377
+ });
378
+ // 3. Repository
379
+ const repo = cliRepo ?? await input({
380
+ message: "Repository name (will be created if it doesn't exist):",
381
+ default: "weekly-report",
382
+ validate: (v) => /^[a-zA-Z0-9._-]+$/.test(v) ? true : "Invalid repository name",
383
+ });
384
+ // 4. Site title
385
+ const siteTitle = await input({
386
+ message: "Site title (shown in header and hero):",
387
+ default: "Dev\\nPulse",
388
+ });
389
+ // 5. Language
390
+ const language = (await select({
391
+ message: "Report language:",
392
+ choices: LANGUAGE_CHOICES,
393
+ default: "en",
394
+ }));
395
+ // 6. Timezone
396
+ const timezone = (await select({
397
+ message: "Timezone (for scheduling and date calculations):",
398
+ choices: [
399
+ ...TIMEZONE_CHOICES,
400
+ { name: "Other (enter manually)", value: "__other__" },
401
+ ],
402
+ default: "UTC",
403
+ }));
404
+ const resolvedTimezone = timezone === "__other__"
405
+ ? await input({
406
+ message: "IANA timezone (e.g. Asia/Tokyo, Europe/London):",
407
+ validate: (v) => {
408
+ try {
409
+ Intl.DateTimeFormat(undefined, { timeZone: v });
410
+ return true;
411
+ }
412
+ catch {
413
+ return "Invalid timezone. See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones";
414
+ }
415
+ },
416
+ })
417
+ : timezone;
418
+ // 7. LLM (required for report generation)
419
+ console.log("\n An LLM provider is required for report generation.");
420
+ console.log(" Groq and OpenRouter offer generous free tiers (no credit card required).\n");
421
+ let llmProvider;
422
+ let llmApiKey;
423
+ let llmModel;
424
+ {
425
+ llmProvider = (await select({
426
+ message: "LLM provider:",
427
+ choices: [
428
+ { name: "OpenRouter - Free tier, 25+ free models (recommended)", value: "openrouter" },
429
+ { name: "Groq - Free tier, fast inference", value: "groq" },
430
+ { name: "Google Gemini - Free tier available", value: "gemini" },
431
+ { name: "OpenAI - Paid", value: "openai" },
432
+ { name: "Anthropic - Paid", value: "anthropic" },
433
+ { name: "Grok (xAI) - Paid", value: "grok" },
434
+ ],
435
+ }));
436
+ const keyUrls = {
437
+ groq: "https://console.groq.com/keys",
438
+ openrouter: "https://openrouter.ai/settings/keys",
439
+ openai: "https://platform.openai.com/api-keys",
440
+ anthropic: "https://console.anthropic.com/settings/keys",
441
+ gemini: "https://aistudio.google.com/apikey",
442
+ grok: "https://console.x.ai",
443
+ };
444
+ console.log(`\n Get your API key at: ${keyUrls[llmProvider]}\n`);
445
+ llmApiKey = await password({
446
+ message: `${llmProvider} API key:`,
447
+ validate: (v) => (v.length > 0 ? true : "API key is required"),
448
+ });
449
+ const modelListUrl = MODEL_LIST_URLS[llmProvider] ?? "";
450
+ console.log(`\n Available models: ${modelListUrl}\n`);
451
+ let modelValidated = false;
452
+ while (!modelValidated) {
453
+ llmModel = await input({
454
+ message: "Model name:",
455
+ validate: (v) => (v.length > 0 ? true : "Model name is required"),
456
+ });
457
+ console.log(" Validating model...");
458
+ const result = await validateModel(llmProvider, llmApiKey, llmModel);
459
+ if (result.valid) {
460
+ console.log(" Model OK.\n");
461
+ modelValidated = true;
462
+ }
463
+ else {
464
+ console.log(` ${result.error}`);
465
+ console.log(` Check available models at: ${modelListUrl}\n`);
466
+ const retry = await confirm({ message: "Try a different model?", default: true });
467
+ if (!retry) {
468
+ throw new Error("Setup cancelled: invalid model name.");
469
+ }
470
+ }
471
+ }
472
+ }
473
+ return {
474
+ token,
475
+ username,
476
+ repo,
477
+ siteTitle,
478
+ language,
479
+ timezone: resolvedTimezone,
480
+ llmProvider,
481
+ llmApiKey,
482
+ llmModel,
483
+ };
484
+ };
485
+ // ── Setup actions ────────────────────────────────────────────
486
+ const step = (msg) => console.log(`\n [*] ${msg}`);
487
+ const ok = (msg) => console.log(` ${msg}`);
488
+ const ensureRepo = async (token, fullRepo) => {
489
+ const res = await ghGet(token, `/repos/${fullRepo}`);
490
+ if (res.ok)
491
+ return false;
492
+ const [owner, name] = fullRepo.split("/");
493
+ const { login } = (await (await ghGet(token, "/user")).json());
494
+ const createRes = owner === login
495
+ ? await ghPost(token, "/user/repos", {
496
+ name,
497
+ auto_init: true,
498
+ private: false,
499
+ description: "Weekly GitHub activity reports",
500
+ })
501
+ : await ghPost(token, `/orgs/${owner}/repos`, {
502
+ name,
503
+ auto_init: true,
504
+ private: false,
505
+ description: "Weekly GitHub activity reports",
506
+ });
507
+ if (!createRes.ok) {
508
+ const body = await createRes.text();
509
+ throw new Error(`Failed to create ${fullRepo}: ${createRes.status}\n ${body}`);
510
+ }
511
+ // Wait for repo to be ready
512
+ await new Promise((r) => setTimeout(r, 2000));
513
+ return true;
514
+ };
515
+ const addFileToRepo = async (token, repo, path, content, message) => {
516
+ const existing = await ghGet(token, `/repos/${repo}/contents/${path}`);
517
+ const sha = existing.ok
518
+ ? (await existing.json()).sha
519
+ : undefined;
520
+ const res = await ghPut(token, `/repos/${repo}/contents/${path}`, {
521
+ message,
522
+ content: btoa(unescape(encodeURIComponent(content))),
523
+ ...(sha ? { sha } : {}),
524
+ });
525
+ if (!res.ok) {
526
+ throw new Error(`Failed to add ${path}: ${res.status}`);
527
+ }
528
+ };
529
+ const enablePages = async (token, repo) => {
530
+ // Ensure gh-pages branch exists
531
+ const mainRef = await ghGet(token, `/repos/${repo}/git/ref/heads/main`);
532
+ if (!mainRef.ok)
533
+ throw new Error("Cannot find main branch");
534
+ const { object } = (await mainRef.json());
535
+ const ghPagesRef = await ghGet(token, `/repos/${repo}/git/ref/heads/gh-pages`);
536
+ if (!ghPagesRef.ok) {
537
+ const createRef = await ghPost(token, `/repos/${repo}/git/refs`, {
538
+ ref: "refs/heads/gh-pages",
539
+ sha: object.sha,
540
+ });
541
+ if (!createRef.ok) {
542
+ throw new Error("Failed to create gh-pages branch");
543
+ }
544
+ }
545
+ // Enable Pages (may already be enabled)
546
+ await ghPost(token, `/repos/${repo}/pages`, {
547
+ source: { branch: "gh-pages", path: "/" },
548
+ });
549
+ const [owner, name] = repo.split("/");
550
+ return `https://${owner}.github.io/${name}`;
551
+ };
552
+ const run = async (cliRepo) => {
553
+ const config = await collectInputs(cliRepo);
554
+ const fullRepo = config.repo.includes("/")
555
+ ? config.repo
556
+ : `${config.username}/${config.repo}`;
557
+ const pagesUrl = `https://${fullRepo.split("/")[0]}.github.io/${fullRepo.split("/")[1]}`;
558
+ const cron = midnightCronUTC(config.timezone);
559
+ // Confirmation
560
+ console.log("\n ── Setup Summary ─────────────────────────────");
561
+ console.log(` Repository: ${fullRepo}`);
562
+ console.log(` Username: ${config.username}`);
563
+ console.log(` Site title: ${config.siteTitle}`);
564
+ console.log(` Language: ${config.language}`);
565
+ console.log(` Timezone: ${config.timezone}`);
566
+ console.log(` Schedule: Daily at midnight (cron: ${cron})`);
567
+ if (config.llmProvider && config.llmModel) {
568
+ console.log(` LLM provider: ${config.llmProvider}`);
569
+ console.log(` LLM model: ${config.llmModel}`);
570
+ }
571
+ else {
572
+ console.log(" LLM: Not configured");
573
+ }
574
+ console.log(` Pages URL: ${pagesUrl}`);
575
+ console.log(" ──────────────────────────────────────────────\n");
576
+ const proceed = await confirm({ message: "Proceed with setup?", default: true });
577
+ if (!proceed) {
578
+ console.log("\n Setup cancelled.\n");
579
+ return;
580
+ }
581
+ // 1. Repository
582
+ step("Setting up repository...");
583
+ const created = await ensureRepo(config.token, fullRepo);
584
+ ok(created ? `Created ${fullRepo}` : `Using existing ${fullRepo}`);
585
+ // 2. PAT secret (needed to read activity across all repos)
586
+ step("Setting secret GH_PAT...");
587
+ const patOk = await setRepoSecret(config.token, fullRepo, "GH_PAT", config.token);
588
+ if (!patOk) {
589
+ throw new Error("Failed to set GH_PAT secret.\n\n" +
590
+ " This is required for the workflow to read activity across all your repos.\n" +
591
+ " Possible causes:\n" +
592
+ " - Fine-grained PAT: make sure 'Secrets: Read and write' permission is granted\n" +
593
+ " and 'Repository access' is set to 'All repositories'\n" +
594
+ " - Classic PAT: make sure 'repo' scope is granted\n\n" +
595
+ ` You can also set it manually at:\n https://github.com/${fullRepo}/settings/secrets/actions`);
596
+ }
597
+ ok("GH_PAT configured.");
598
+ // 3. LLM secret
599
+ if (config.llmProvider && config.llmApiKey) {
600
+ const secretName = LLM_SECRET_NAMES[config.llmProvider];
601
+ step(`Setting secret ${secretName}...`);
602
+ const llmOk = await setRepoSecret(config.token, fullRepo, secretName, config.llmApiKey);
603
+ if (!llmOk) {
604
+ throw new Error(`Failed to set ${secretName} secret.\n\n` +
605
+ " This is required for AI narrative generation.\n" +
606
+ ` You can set it manually at:\n https://github.com/${fullRepo}/settings/secrets/actions`);
607
+ }
608
+ ok("Secret configured.");
609
+ }
610
+ // 4. Workflows
611
+ step("Adding workflows...");
612
+ const workflowOpts = {
613
+ username: config.username,
614
+ language: config.language,
615
+ timezone: config.timezone,
616
+ siteTitle: config.siteTitle,
617
+ llmProvider: config.llmProvider,
618
+ llmModel: config.llmModel,
619
+ llmSecretName: config.llmProvider
620
+ ? LLM_SECRET_NAMES[config.llmProvider]
621
+ : undefined,
622
+ };
623
+ await addFileToRepo(config.token, fullRepo, ".github/workflows/daily-fetch.yml", buildDailyWorkflow(workflowOpts), "chore: add daily fetch workflow");
624
+ ok("daily-fetch.yml added.");
625
+ await addFileToRepo(config.token, fullRepo, ".github/workflows/weekly-report.yml", buildWeeklyWorkflow(workflowOpts), "chore: add weekly report workflow");
626
+ ok("weekly-report.yml added.");
627
+ // 5. README
628
+ step("Adding README...");
629
+ const readme = buildReadme({
630
+ siteTitle: config.siteTitle,
631
+ username: config.username,
632
+ repo: fullRepo,
633
+ pagesUrl,
634
+ language: config.language,
635
+ timezone: config.timezone,
636
+ llmProvider: config.llmProvider,
637
+ llmModel: config.llmModel,
638
+ });
639
+ await addFileToRepo(config.token, fullRepo, "README.md", readme, "docs: add README with configuration guide");
640
+ ok("README added.");
641
+ // 6. GitHub Pages
642
+ step("Enabling GitHub Pages...");
643
+ try {
644
+ const url = await enablePages(config.token, fullRepo);
645
+ ok(`Pages enabled: ${url}`);
646
+ }
647
+ catch {
648
+ ok("Pages may already be enabled or require manual setup.");
649
+ ok(`Enable at: https://github.com/${fullRepo}/settings/pages`);
650
+ }
651
+ // 7. Trigger first weekly report
652
+ step("Generating first weekly report...");
653
+ const dispatchRes = await ghPost(config.token, `/repos/${fullRepo}/actions/workflows/weekly-report.yml/dispatches`, { ref: "main" });
654
+ let runUrl = `https://github.com/${fullRepo}/actions`;
655
+ if (dispatchRes.ok) {
656
+ ok("Weekly report workflow triggered.");
657
+ // Wait briefly then fetch the latest run URL
658
+ await sleep(3000);
659
+ const runsRes = await ghGet(config.token, `/repos/${fullRepo}/actions/workflows/weekly-report.yml/runs?per_page=1`);
660
+ if (runsRes.ok) {
661
+ const runs = (await runsRes.json());
662
+ if (runs.workflow_runs.length > 0) {
663
+ runUrl = runs.workflow_runs[0].html_url;
664
+ }
665
+ }
666
+ ok(`Progress: ${runUrl}`);
667
+ }
668
+ else {
669
+ ok("Could not auto-trigger. Run manually:");
670
+ ok(`${runUrl} > Weekly Report > Run workflow`);
671
+ }
672
+ // Done
673
+ console.log(`
674
+ ──────────────────────────────────────────────
675
+ Setup complete!
676
+ ──────────────────────────────────────────────
677
+
678
+ Repository: https://github.com/${fullRepo}
679
+ Triggered build: ${runUrl}
680
+
681
+ Your first weekly report is being generated now.
682
+ Once complete, it will be available at:
683
+ ${pagesUrl}
684
+
685
+ Daily fetches will run automatically at midnight ${config.timezone}.
686
+
687
+ To change settings, edit:
688
+ .github/workflows/daily-fetch.yml (schedule, timezone)
689
+ .github/workflows/weekly-report.yml (LLM, language, site title)
690
+ `);
691
+ };
692
+ // ── Command registration ─────────────────────────────────────
693
+ export const registerSetup = (program) => {
694
+ program
695
+ .command("setup")
696
+ .description("Interactive setup: create repo, add workflow, configure secrets, enable Pages")
697
+ .option("--repo <owner/repo>", "Use existing repository (skip repo creation prompt)")
698
+ .action(async (opts) => {
699
+ try {
700
+ await run(opts.repo);
701
+ }
702
+ catch (error) {
703
+ const msg = error instanceof Error ? error.message : String(error);
704
+ console.error(`\n Error: ${msg}\n`);
705
+ process.exit(1);
706
+ }
707
+ });
708
+ };
709
+ //# sourceMappingURL=setup.js.map