@stats-forge/github-stats-forge-cli 0.1.0 → 0.3.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/build/cards.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface CardOption {
14
14
  /** What the prompt asks. */
15
15
  label: string;
16
16
  kind: OptionKind;
17
- /** The accepted values, for `choice`. */
17
+ /** The accepted values, for `choice` and for a `list` whose values are a closed set. */
18
18
  choices?: ReadonlyArray<string>;
19
19
  /** Shown under the prompt, for anything the label cannot say. */
20
20
  hint?: string;
@@ -32,6 +32,8 @@ export interface CardKind {
32
32
  /** @returns The rendered card, or the rendered error. */
33
33
  render: (query: Record<string, string>, config: CardConfig) => Promise<ApiResult>;
34
34
  }
35
+ /** Colors and the theme, which every card accepts. */
36
+ export declare const COMMON_OPTIONS: ReadonlyArray<CardOption>;
35
37
  /** Every card, with the options every card shares appended to its own. */
36
38
  export declare const cards: ReadonlyArray<CardKind>;
37
39
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../src/cards.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAEtF;;;;;GAKG;AAEH,iFAAiF;AACjF,KAAK,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEpE,6BAA6B;AAC7B,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,CAAC;IACjB,yCAAyC;IACzC,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAChC,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,UAAU,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACpC,+CAA+C;IAC/C,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,yDAAyD;IACzD,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;CACnF;AAkSD,0EAA0E;AAC1E,eAAO,MAAM,KAAK,EAAE,aAAa,CAAC,QAAQ,CAGvC,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,QAAQ,GAAG,SAAiD,CAAC"}
1
+ {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../src/cards.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAEtF;;;;;GAKG;AAEH,iFAAiF;AACjF,KAAK,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEpE,6BAA6B;AAC7B,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,UAAU,CAAC;IACjB,wFAAwF;IACxF,OAAO,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAChC,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,UAAU,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACpC,+CAA+C;IAC/C,OAAO,EAAE,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,yDAAyD;IACzD,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;CACnF;AAID,sDAAsD;AACtD,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,UAAU,CA8BpD,CAAC;AAkTF,0EAA0E;AAC1E,eAAO,MAAM,KAAK,EAAE,aAAa,CAAC,QAAQ,CAGvC,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,QAAQ,OAAQ,MAAM,KAAG,QAAQ,GAAG,SAAiD,CAAC"}
package/build/cards.js CHANGED
@@ -1,7 +1,7 @@
1
- import { gist, pin, stats, themes, topLangs, wakatime, } from '@stats-forge/github-stats-forge-core/api';
1
+ import { contributedTo, gist, pin, stats, themes, topLangs, wakatime, } from '@stats-forge/github-stats-forge-core/api';
2
2
  const THEME_NAMES = Object.keys(themes);
3
3
  /** Colors and the theme, which every card accepts. */
4
- const COMMON_OPTIONS = [
4
+ export const COMMON_OPTIONS = [
5
5
  {
6
6
  name: 'theme',
7
7
  label: 'Theme',
@@ -32,6 +32,18 @@ const COMMON_OPTIONS = [
32
32
  { name: 'border_radius', label: 'Border radius', kind: 'number' },
33
33
  { name: 'hide_border', label: 'Hide the border', kind: 'boolean' },
34
34
  ];
35
+ /**
36
+ * The two ends of a card's date range.
37
+ *
38
+ * @returns The pair, naming what it counts.
39
+ */
40
+ const rangeOptions = (counted) => {
41
+ const hint = 'A year, a month or a day: 2024, 2024-03, 2024-03-15';
42
+ return [
43
+ { name: 'from', label: `Count ${counted} from`, kind: 'text', hint },
44
+ { name: 'to', label: `Count ${counted} up to`, kind: 'text', hint },
45
+ ];
46
+ };
35
47
  const LOCALE_OPTION = {
36
48
  name: 'locale',
37
49
  label: 'Locale',
@@ -49,13 +61,13 @@ const CARDS = [
49
61
  name: 'show',
50
62
  label: 'Extra stats to show',
51
63
  kind: 'list',
52
- hint: 'e.g. reviews,discussions_started,prs_merged,contributions',
64
+ choices: stats.OPTIONS.show,
53
65
  },
54
66
  {
55
67
  name: 'hide',
56
68
  label: 'Stats to hide',
57
69
  kind: 'list',
58
- hint: 'e.g. stars,commits,prs,issues,contribs',
70
+ choices: stats.OPTIONS.hide,
59
71
  },
60
72
  { name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
61
73
  { name: 'hide_rank', label: 'Hide the rank circle', kind: 'boolean' },
@@ -63,19 +75,14 @@ const CARDS = [
63
75
  name: 'rank_icon',
64
76
  label: 'Rank indicator',
65
77
  kind: 'choice',
66
- choices: stats.RANK_ICONS,
78
+ choices: stats.OPTIONS.rank_icon,
67
79
  },
68
80
  {
69
81
  name: 'include_all_commits',
70
82
  label: 'Count commits of all time',
71
83
  kind: 'boolean',
72
84
  },
73
- {
74
- name: 'commits_year',
75
- label: 'Count commits for one year',
76
- kind: 'number',
77
- hint: 'Four digits, e.g. 2025',
78
- },
85
+ ...rangeOptions('commits'),
79
86
  {
80
87
  name: 'exclude_repo',
81
88
  label: 'Repositories to exclude',
@@ -95,7 +102,7 @@ const CARDS = [
95
102
  name: 'role',
96
103
  label: 'Owner affiliations to include',
97
104
  kind: 'list',
98
- hint: 'OWNER, COLLABORATOR, ORGANIZATION_MEMBER',
105
+ choices: stats.OPTIONS.role,
99
106
  },
100
107
  {
101
108
  name: 'contribs_include_own_repos',
@@ -111,7 +118,7 @@ const CARDS = [
111
118
  name: 'number_format',
112
119
  label: 'Number format',
113
120
  kind: 'choice',
114
- choices: ['short', 'long'],
121
+ choices: stats.OPTIONS.number_format,
115
122
  },
116
123
  {
117
124
  name: 'number_precision',
@@ -126,7 +133,7 @@ const CARDS = [
126
133
  { name: 'ring_color', label: 'Rank ring color', kind: 'text' },
127
134
  LOCALE_OPTION,
128
135
  ],
129
- render: (query, config) => stats(query, config),
136
+ render: stats,
130
137
  },
131
138
  {
132
139
  id: 'top-langs',
@@ -138,7 +145,7 @@ const CARDS = [
138
145
  name: 'layout',
139
146
  label: 'Layout',
140
147
  kind: 'choice',
141
- choices: topLangs.LAYOUTS,
148
+ choices: topLangs.OPTIONS.layout,
142
149
  },
143
150
  { name: 'langs_count', label: 'Languages to show', kind: 'number' },
144
151
  { name: 'hide', label: 'Languages to hide', kind: 'list' },
@@ -157,7 +164,7 @@ const CARDS = [
157
164
  name: 'stats_format',
158
165
  label: 'Show values as',
159
166
  kind: 'choice',
160
- choices: topLangs.STATS_FORMATS,
167
+ choices: topLangs.OPTIONS.stats_format,
161
168
  },
162
169
  {
163
170
  name: 'hide_progress',
@@ -170,7 +177,12 @@ const CARDS = [
170
177
  label: 'Progress bar background color',
171
178
  kind: 'text',
172
179
  },
173
- { name: 'role', label: 'Owner affiliations to include', kind: 'list' },
180
+ {
181
+ name: 'role',
182
+ label: 'Owner affiliations to include',
183
+ kind: 'list',
184
+ choices: topLangs.OPTIONS.role,
185
+ },
174
186
  { name: 'custom_title', label: 'Card title', kind: 'text' },
175
187
  { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
176
188
  { name: 'card_width', label: 'Card width', kind: 'number' },
@@ -181,7 +193,7 @@ const CARDS = [
181
193
  },
182
194
  LOCALE_OPTION,
183
195
  ],
184
- render: (query, config) => topLangs(query, config),
196
+ render: topLangs,
185
197
  },
186
198
  {
187
199
  id: 'pin',
@@ -197,7 +209,7 @@ const CARDS = [
197
209
  name: 'show',
198
210
  label: 'Extra stats to show',
199
211
  kind: 'list',
200
- hint: 'e.g. prs_authored,issues_commented',
212
+ choices: pin.OPTIONS.show,
201
213
  },
202
214
  { name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
203
215
  {
@@ -217,11 +229,48 @@ const CARDS = [
217
229
  name: 'number_format',
218
230
  label: 'Number format',
219
231
  kind: 'choice',
220
- choices: ['short', 'long'],
232
+ choices: pin.OPTIONS.number_format,
233
+ },
234
+ LOCALE_OPTION,
235
+ ],
236
+ render: pin,
237
+ },
238
+ {
239
+ id: 'contributed-to',
240
+ label: 'Contributed to — repositories you work on, ranked',
241
+ needsToken: true,
242
+ required: [{ name: 'username', label: 'GitHub username', kind: 'text' }],
243
+ options: [
244
+ { name: 'repos_count', label: 'Repositories to show', kind: 'number' },
245
+ {
246
+ name: 'include_own_repos',
247
+ label: 'Include your own repositories',
248
+ kind: 'boolean',
249
+ },
250
+ {
251
+ name: 'exclude_repo',
252
+ label: 'Repositories to exclude',
253
+ kind: 'list',
254
+ hint: 'Each one an owner/name, or just the name',
255
+ },
256
+ ...rangeOptions('contributions'),
257
+ {
258
+ name: 'hide_years',
259
+ label: 'Hide the year marks',
260
+ kind: 'boolean',
261
+ hint: 'One mark per contribution year, filled for the years that repo got one',
262
+ },
263
+ { name: 'custom_title', label: 'Card title', kind: 'text' },
264
+ { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
265
+ { name: 'card_width', label: 'Card width', kind: 'number' },
266
+ {
267
+ name: 'disable_animations',
268
+ label: 'Disable the animations',
269
+ kind: 'boolean',
221
270
  },
222
271
  LOCALE_OPTION,
223
272
  ],
224
- render: (query, config) => pin(query, config),
273
+ render: contributedTo,
225
274
  },
226
275
  {
227
276
  id: 'gist',
@@ -235,8 +284,9 @@ const CARDS = [
235
284
  label: 'Let the browser wrap the description',
236
285
  kind: 'boolean',
237
286
  },
287
+ LOCALE_OPTION,
238
288
  ],
239
- render: (query, config) => gist(query, config),
289
+ render: gist,
240
290
  },
241
291
  {
242
292
  id: 'wakatime',
@@ -248,13 +298,13 @@ const CARDS = [
248
298
  name: 'layout',
249
299
  label: 'Layout',
250
300
  kind: 'choice',
251
- choices: wakatime.LAYOUTS,
301
+ choices: wakatime.OPTIONS.layout,
252
302
  },
253
303
  {
254
304
  name: 'display_format',
255
305
  label: 'Show values as',
256
306
  kind: 'choice',
257
- choices: wakatime.DISPLAY_FORMATS,
307
+ choices: wakatime.OPTIONS.display_format,
258
308
  },
259
309
  { name: 'langs_count', label: 'Languages to show', kind: 'number' },
260
310
  { name: 'hide', label: 'Languages to hide', kind: 'list' },
@@ -280,7 +330,7 @@ const CARDS = [
280
330
  },
281
331
  LOCALE_OPTION,
282
332
  ],
283
- render: (query, config) => wakatime(query, config),
333
+ render: wakatime,
284
334
  },
285
335
  ];
286
336
  /** Every card, with the options every card shares appended to its own. */
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;;;;GAKG;AAEH,mCAAmC;AACnC,eAAO,MAAM,QAAQ,QAAO,OAAO,CAAC,QAAQ,CAIxC,CAAC;AA6BL,gDAAgD;AAChD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEtD,sDAAsD;AACtD,MAAM,WAAW,IAAI;IACnB,uCAAuC;IACvC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,SACpB,QAAQ,QACR,IAAI,WACD,MAAM,KACd,OAAO,CAAC,UAAU,CAmCpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,SAAgB,QAAQ,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,eAAsB,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAMhF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC,MAAM,GAAG,SAAS,CAM3D,CAAC"}
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;;;;GAKG;AAEH,mCAAmC;AACnC,eAAO,MAAM,QAAQ,QAAO,OAAO,CAAC,QAAQ,CAIxC,CAAC;AAuCL,gDAAgD;AAChD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEtD,sDAAsD;AACtD,MAAM,WAAW,IAAI;IACnB,uCAAuC;IACvC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,SACpB,QAAQ,QACR,IAAI,WACD,MAAM,KACd,OAAO,CAAC,UAAU,CAmCpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,SAAgB,QAAQ,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,eAAsB,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAMhF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC,MAAM,GAAG,SAAS,CAM3D,CAAC"}
package/build/prompts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { confirm, input, password, select } from '@inquirer/prompts';
1
+ import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
2
2
  import { cards } from './cards.js';
3
3
  import { describeAnswer } from './query.js';
4
4
  /**
@@ -22,6 +22,15 @@ const askOption = async (option, current) => {
22
22
  if (option.kind === 'boolean') {
23
23
  return confirm({ message, default: current === true });
24
24
  }
25
+ if (option.kind === 'list' && option.choices) {
26
+ const chosen = new Set(Array.isArray(current) ? current : []);
27
+ const picked = await checkbox({
28
+ message,
29
+ pageSize: 15,
30
+ choices: option.choices.map((value) => ({ value, checked: chosen.has(value) })),
31
+ });
32
+ return picked.length > 0 ? picked : undefined;
33
+ }
25
34
  if (option.kind === 'choice') {
26
35
  const choices = [
27
36
  { name: '— leave unset —', value: undefined },
@@ -25,7 +25,8 @@ export declare const writeSavedCard: (path: string, card: CardKind, options: Rec
25
25
  * Turns saved options back into answers the menu can show and edit.
26
26
  *
27
27
  * Everything on a query string is a string;
28
- * a boolean option becomes one again so its prompt opens on the right answer.
28
+ * a boolean becomes one again, and a list splits back into its values,
29
+ * so each prompt opens on the answer it was saved with.
29
30
  *
30
31
  * @returns The answers, ready for the menu.
31
32
  */
@@ -1 +1 @@
1
- {"version":3,"file":"saved-card.d.ts","sourceRoot":"","sources":["../src/saved-card.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAiBzC;;GAEG;AACH,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,OAAmD,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,SAClB,MAAM,KACX,OAAO,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAyB7D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,SACnB,MAAM,QACN,QAAQ,WACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,OAAO,CAAC,MAAM,CAKhB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,SAAU,QAAQ,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAW7F,CAAC"}
1
+ {"version":3,"file":"saved-card.d.ts","sourceRoot":"","sources":["../src/saved-card.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAiBzC;;GAEG;AACH,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,OAAmD,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,SAClB,MAAM,KACX,OAAO,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAyB7D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,SACnB,MAAM,QACN,QAAQ,WACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B,OAAO,CAAC,MAAM,CAKhB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,SAAU,QAAQ,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAuB7F,CAAC"}
@@ -49,14 +49,28 @@ export const writeSavedCard = async (path, card, options) => {
49
49
  * Turns saved options back into answers the menu can show and edit.
50
50
  *
51
51
  * Everything on a query string is a string;
52
- * a boolean option becomes one again so its prompt opens on the right answer.
52
+ * a boolean becomes one again, and a list splits back into its values,
53
+ * so each prompt opens on the answer it was saved with.
53
54
  *
54
55
  * @returns The answers, ready for the menu.
55
56
  */
56
57
  export const toAnswers = (card, options) => {
57
58
  const kinds = new Map([...card.required, ...card.options].map((option) => [option.name, option.kind]));
58
- return new Map(Object.entries(options).map(([name, value]) => [
59
- name,
60
- kinds.get(name) === 'boolean' ? value === 'true' : value,
61
- ]));
59
+ const toAnswer = (name, value) => {
60
+ switch (kinds.get(name)) {
61
+ case 'boolean': {
62
+ return value === 'true';
63
+ }
64
+ case 'list': {
65
+ return value
66
+ .split(',')
67
+ .map((item) => item.trim())
68
+ .filter(Boolean);
69
+ }
70
+ default: {
71
+ return value;
72
+ }
73
+ }
74
+ };
75
+ return new Map(Object.entries(options).map(([name, value]) => [name, toAnswer(name, value)]));
62
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stats-forge/github-stats-forge-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Render a GitHub stats card to a local SVG file, one prompt at a time",
5
5
  "keywords": [
6
6
  "cli",
@@ -34,6 +34,10 @@
34
34
  ".": {
35
35
  "@stats/source": "./src/index.ts",
36
36
  "default": "./build/index.js"
37
+ },
38
+ "./cards": {
39
+ "@stats/source": "./src/cards.ts",
40
+ "default": "./build/cards.js"
37
41
  }
38
42
  },
39
43
  "publishConfig": {
@@ -41,7 +45,7 @@
41
45
  },
42
46
  "dependencies": {
43
47
  "@inquirer/prompts": "8.7.0",
44
- "@stats-forge/github-stats-forge-core": "^0.1.0"
48
+ "@stats-forge/github-stats-forge-core": "^0.3.0"
45
49
  },
46
50
  "devDependencies": {
47
51
  "vitest": "4.1.11"
package/src/cards.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ contributedTo,
2
3
  gist,
3
4
  pin,
4
5
  stats,
@@ -25,7 +26,7 @@ export interface CardOption {
25
26
  /** What the prompt asks. */
26
27
  label: string;
27
28
  kind: OptionKind;
28
- /** The accepted values, for `choice`. */
29
+ /** The accepted values, for `choice` and for a `list` whose values are a closed set. */
29
30
  choices?: ReadonlyArray<string>;
30
31
  /** Shown under the prompt, for anything the label cannot say. */
31
32
  hint?: string;
@@ -48,7 +49,7 @@ export interface CardKind {
48
49
  const THEME_NAMES = Object.keys(themes);
49
50
 
50
51
  /** Colors and the theme, which every card accepts. */
51
- const COMMON_OPTIONS: ReadonlyArray<CardOption> = [
52
+ export const COMMON_OPTIONS: ReadonlyArray<CardOption> = [
52
53
  {
53
54
  name: 'theme',
54
55
  label: 'Theme',
@@ -80,6 +81,19 @@ const COMMON_OPTIONS: ReadonlyArray<CardOption> = [
80
81
  { name: 'hide_border', label: 'Hide the border', kind: 'boolean' },
81
82
  ];
82
83
 
84
+ /**
85
+ * The two ends of a card's date range.
86
+ *
87
+ * @returns The pair, naming what it counts.
88
+ */
89
+ const rangeOptions = (counted: string): ReadonlyArray<CardOption> => {
90
+ const hint = 'A year, a month or a day: 2024, 2024-03, 2024-03-15';
91
+ return [
92
+ { name: 'from', label: `Count ${counted} from`, kind: 'text', hint },
93
+ { name: 'to', label: `Count ${counted} up to`, kind: 'text', hint },
94
+ ];
95
+ };
96
+
83
97
  const LOCALE_OPTION: CardOption = {
84
98
  name: 'locale',
85
99
  label: 'Locale',
@@ -98,13 +112,13 @@ const CARDS: ReadonlyArray<CardKind> = [
98
112
  name: 'show',
99
113
  label: 'Extra stats to show',
100
114
  kind: 'list',
101
- hint: 'e.g. reviews,discussions_started,prs_merged,contributions',
115
+ choices: stats.OPTIONS.show,
102
116
  },
103
117
  {
104
118
  name: 'hide',
105
119
  label: 'Stats to hide',
106
120
  kind: 'list',
107
- hint: 'e.g. stars,commits,prs,issues,contribs',
121
+ choices: stats.OPTIONS.hide,
108
122
  },
109
123
  { name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
110
124
  { name: 'hide_rank', label: 'Hide the rank circle', kind: 'boolean' },
@@ -112,19 +126,14 @@ const CARDS: ReadonlyArray<CardKind> = [
112
126
  name: 'rank_icon',
113
127
  label: 'Rank indicator',
114
128
  kind: 'choice',
115
- choices: stats.RANK_ICONS,
129
+ choices: stats.OPTIONS.rank_icon,
116
130
  },
117
131
  {
118
132
  name: 'include_all_commits',
119
133
  label: 'Count commits of all time',
120
134
  kind: 'boolean',
121
135
  },
122
- {
123
- name: 'commits_year',
124
- label: 'Count commits for one year',
125
- kind: 'number',
126
- hint: 'Four digits, e.g. 2025',
127
- },
136
+ ...rangeOptions('commits'),
128
137
  {
129
138
  name: 'exclude_repo',
130
139
  label: 'Repositories to exclude',
@@ -144,7 +153,7 @@ const CARDS: ReadonlyArray<CardKind> = [
144
153
  name: 'role',
145
154
  label: 'Owner affiliations to include',
146
155
  kind: 'list',
147
- hint: 'OWNER, COLLABORATOR, ORGANIZATION_MEMBER',
156
+ choices: stats.OPTIONS.role,
148
157
  },
149
158
  {
150
159
  name: 'contribs_include_own_repos',
@@ -160,7 +169,7 @@ const CARDS: ReadonlyArray<CardKind> = [
160
169
  name: 'number_format',
161
170
  label: 'Number format',
162
171
  kind: 'choice',
163
- choices: ['short', 'long'],
172
+ choices: stats.OPTIONS.number_format,
164
173
  },
165
174
  {
166
175
  name: 'number_precision',
@@ -175,7 +184,7 @@ const CARDS: ReadonlyArray<CardKind> = [
175
184
  { name: 'ring_color', label: 'Rank ring color', kind: 'text' },
176
185
  LOCALE_OPTION,
177
186
  ],
178
- render: (query, config) => stats(query, config),
187
+ render: stats,
179
188
  },
180
189
  {
181
190
  id: 'top-langs',
@@ -187,7 +196,7 @@ const CARDS: ReadonlyArray<CardKind> = [
187
196
  name: 'layout',
188
197
  label: 'Layout',
189
198
  kind: 'choice',
190
- choices: topLangs.LAYOUTS,
199
+ choices: topLangs.OPTIONS.layout,
191
200
  },
192
201
  { name: 'langs_count', label: 'Languages to show', kind: 'number' },
193
202
  { name: 'hide', label: 'Languages to hide', kind: 'list' },
@@ -206,7 +215,7 @@ const CARDS: ReadonlyArray<CardKind> = [
206
215
  name: 'stats_format',
207
216
  label: 'Show values as',
208
217
  kind: 'choice',
209
- choices: topLangs.STATS_FORMATS,
218
+ choices: topLangs.OPTIONS.stats_format,
210
219
  },
211
220
  {
212
221
  name: 'hide_progress',
@@ -219,7 +228,12 @@ const CARDS: ReadonlyArray<CardKind> = [
219
228
  label: 'Progress bar background color',
220
229
  kind: 'text',
221
230
  },
222
- { name: 'role', label: 'Owner affiliations to include', kind: 'list' },
231
+ {
232
+ name: 'role',
233
+ label: 'Owner affiliations to include',
234
+ kind: 'list',
235
+ choices: topLangs.OPTIONS.role,
236
+ },
223
237
  { name: 'custom_title', label: 'Card title', kind: 'text' },
224
238
  { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
225
239
  { name: 'card_width', label: 'Card width', kind: 'number' },
@@ -230,7 +244,7 @@ const CARDS: ReadonlyArray<CardKind> = [
230
244
  },
231
245
  LOCALE_OPTION,
232
246
  ],
233
- render: (query, config) => topLangs(query, config),
247
+ render: topLangs,
234
248
  },
235
249
  {
236
250
  id: 'pin',
@@ -246,7 +260,7 @@ const CARDS: ReadonlyArray<CardKind> = [
246
260
  name: 'show',
247
261
  label: 'Extra stats to show',
248
262
  kind: 'list',
249
- hint: 'e.g. prs_authored,issues_commented',
263
+ choices: pin.OPTIONS.show,
250
264
  },
251
265
  { name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
252
266
  {
@@ -266,11 +280,48 @@ const CARDS: ReadonlyArray<CardKind> = [
266
280
  name: 'number_format',
267
281
  label: 'Number format',
268
282
  kind: 'choice',
269
- choices: ['short', 'long'],
283
+ choices: pin.OPTIONS.number_format,
284
+ },
285
+ LOCALE_OPTION,
286
+ ],
287
+ render: pin,
288
+ },
289
+ {
290
+ id: 'contributed-to',
291
+ label: 'Contributed to — repositories you work on, ranked',
292
+ needsToken: true,
293
+ required: [{ name: 'username', label: 'GitHub username', kind: 'text' }],
294
+ options: [
295
+ { name: 'repos_count', label: 'Repositories to show', kind: 'number' },
296
+ {
297
+ name: 'include_own_repos',
298
+ label: 'Include your own repositories',
299
+ kind: 'boolean',
300
+ },
301
+ {
302
+ name: 'exclude_repo',
303
+ label: 'Repositories to exclude',
304
+ kind: 'list',
305
+ hint: 'Each one an owner/name, or just the name',
306
+ },
307
+ ...rangeOptions('contributions'),
308
+ {
309
+ name: 'hide_years',
310
+ label: 'Hide the year marks',
311
+ kind: 'boolean',
312
+ hint: 'One mark per contribution year, filled for the years that repo got one',
313
+ },
314
+ { name: 'custom_title', label: 'Card title', kind: 'text' },
315
+ { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
316
+ { name: 'card_width', label: 'Card width', kind: 'number' },
317
+ {
318
+ name: 'disable_animations',
319
+ label: 'Disable the animations',
320
+ kind: 'boolean',
270
321
  },
271
322
  LOCALE_OPTION,
272
323
  ],
273
- render: (query, config) => pin(query, config),
324
+ render: contributedTo,
274
325
  },
275
326
  {
276
327
  id: 'gist',
@@ -284,8 +335,9 @@ const CARDS: ReadonlyArray<CardKind> = [
284
335
  label: 'Let the browser wrap the description',
285
336
  kind: 'boolean',
286
337
  },
338
+ LOCALE_OPTION,
287
339
  ],
288
- render: (query, config) => gist(query, config),
340
+ render: gist,
289
341
  },
290
342
  {
291
343
  id: 'wakatime',
@@ -297,13 +349,13 @@ const CARDS: ReadonlyArray<CardKind> = [
297
349
  name: 'layout',
298
350
  label: 'Layout',
299
351
  kind: 'choice',
300
- choices: wakatime.LAYOUTS,
352
+ choices: wakatime.OPTIONS.layout,
301
353
  },
302
354
  {
303
355
  name: 'display_format',
304
356
  label: 'Show values as',
305
357
  kind: 'choice',
306
- choices: wakatime.DISPLAY_FORMATS,
358
+ choices: wakatime.OPTIONS.display_format,
307
359
  },
308
360
  { name: 'langs_count', label: 'Languages to show', kind: 'number' },
309
361
  { name: 'hide', label: 'Languages to hide', kind: 'list' },
@@ -329,7 +381,7 @@ const CARDS: ReadonlyArray<CardKind> = [
329
381
  },
330
382
  LOCALE_OPTION,
331
383
  ],
332
- render: (query, config) => wakatime(query, config),
384
+ render: wakatime,
333
385
  },
334
386
  ];
335
387
 
package/src/prompts.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { confirm, input, password, select } from '@inquirer/prompts';
1
+ import { checkbox, confirm, input, password, select } from '@inquirer/prompts';
2
2
 
3
3
  import type { CardKind, CardOption } from './cards.ts';
4
4
  import { cards } from './cards.ts';
@@ -31,6 +31,16 @@ const askOption = async (option: CardOption, current: Answer): Promise<Answer> =
31
31
  return confirm({ message, default: current === true });
32
32
  }
33
33
 
34
+ if (option.kind === 'list' && option.choices) {
35
+ const chosen = new Set(Array.isArray(current) ? current : []);
36
+ const picked = await checkbox({
37
+ message,
38
+ pageSize: 15,
39
+ choices: option.choices.map((value) => ({ value, checked: chosen.has(value) })),
40
+ });
41
+ return picked.length > 0 ? picked : undefined;
42
+ }
43
+
34
44
  if (option.kind === 'choice') {
35
45
  const choices = [
36
46
  { name: '— leave unset —', value: undefined as Answer },
package/src/saved-card.ts CHANGED
@@ -15,7 +15,7 @@ import type { Answer } from './query.ts';
15
15
 
16
16
  /** A card and the answers it was rendered from: the shape of the file. */
17
17
  interface SavedCard {
18
- /** Which card: `stats`, `top-langs`, `pin`, `gist`, `wakatime`. */
18
+ /** Which card, by the id the catalog gives it. */
19
19
  card: string;
20
20
  /** The options, exactly as they reach the endpoint. */
21
21
  options: Record<string, string>;
@@ -82,7 +82,8 @@ export const writeSavedCard = async (
82
82
  * Turns saved options back into answers the menu can show and edit.
83
83
  *
84
84
  * Everything on a query string is a string;
85
- * a boolean option becomes one again so its prompt opens on the right answer.
85
+ * a boolean becomes one again, and a list splits back into its values,
86
+ * so each prompt opens on the answer it was saved with.
86
87
  *
87
88
  * @returns The answers, ready for the menu.
88
89
  */
@@ -91,10 +92,22 @@ export const toAnswers = (card: CardKind, options: Record<string, string>): Map<
91
92
  [...card.required, ...card.options].map((option) => [option.name, option.kind]),
92
93
  );
93
94
 
94
- return new Map(
95
- Object.entries(options).map(([name, value]) => [
96
- name,
97
- kinds.get(name) === 'boolean' ? value === 'true' : value,
98
- ]),
99
- );
95
+ const toAnswer = (name: string, value: string): Answer => {
96
+ switch (kinds.get(name)) {
97
+ case 'boolean': {
98
+ return value === 'true';
99
+ }
100
+ case 'list': {
101
+ return value
102
+ .split(',')
103
+ .map((item) => item.trim())
104
+ .filter(Boolean);
105
+ }
106
+ default: {
107
+ return value;
108
+ }
109
+ }
110
+ };
111
+
112
+ return new Map(Object.entries(options).map(([name, value]) => [name, toAnswer(name, value)]));
100
113
  };