@stats-forge/github-stats-forge-cli 0.0.2 → 0.1.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/README.md CHANGED
@@ -21,7 +21,7 @@ Requires Node 24 or newer. The cards themselves come from
21
21
  so every option below is the one that package's api accepts — the prompts read the
22
22
  accepted values off it rather than keeping their own copy.
23
23
 
24
- It asks which card you want, then the params that card cannot render without, then
24
+ It asks which card you want, then the options that card cannot render without, then
25
25
  puts you in a menu of every other option it accepts — pick one, answer it, and the
26
26
  menu comes back with the answer beside it. `Generate the card` writes the SVG.
27
27
 
@@ -43,7 +43,7 @@ again. `Quit` (or Ctrl-C) ends the session.
43
43
 
44
44
  ## Saving a card
45
45
 
46
- `Save these options` in the menu writes the card and its params to a JSON file, and
46
+ `Save these options` in the menu writes the card and its options to a JSON file, and
47
47
  `--config` loads one back:
48
48
 
49
49
  ```sh
@@ -53,7 +53,7 @@ github-stats-forge --config my-stats.json # picks up where the file left of
53
53
  ```json
54
54
  {
55
55
  "card": "stats",
56
- "params": {
56
+ "options": {
57
57
  "username": "anuraghazra",
58
58
  "theme": "tokyonight",
59
59
  "show_icons": "true"
@@ -63,7 +63,7 @@ github-stats-forge --config my-stats.json # picks up where the file left of
63
63
 
64
64
  The file holds what a query string holds, so it reads like the URL it stands for and
65
65
  can be edited by hand. Loading one skips both the card prompt and the required
66
- params, landing you straight on the menu — and `--config` doubles as the save target,
66
+ options, landing you straight on the menu — and `--config` doubles as the save target,
67
67
  so the next `Save these options` writes back to it without asking.
68
68
 
69
69
  `--generate` skips the menu too, rendering exactly what the file holds:
package/build/cards.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ApiResult, CardConfig } from '@stats-forge/github-stats-forge-core';
1
+ import type { ApiResult, CardConfig } from '@stats-forge/github-stats-forge-core/api';
2
2
  /**
3
3
  * @file What each card accepts, in the order the prompts walk it.
4
4
  *
@@ -29,17 +29,12 @@ export interface CardKind {
29
29
  required: ReadonlyArray<CardOption>;
30
30
  /** Everything else, navigable in any order. */
31
31
  options: ReadonlyArray<CardOption>;
32
- /**
33
- * @param query The answers, as a query string would carry them.
34
- * @param config Tokens the fetchers use.
35
- * @returns The rendered card, or the rendered error.
36
- */
32
+ /** @returns The rendered card, or the rendered error. */
37
33
  render: (query: Record<string, string>, config: CardConfig) => Promise<ApiResult>;
38
34
  }
39
35
  /** Every card, with the options every card shares appended to its own. */
40
36
  export declare const cards: ReadonlyArray<CardKind>;
41
37
  /**
42
- * @param id The card's id, as `--card` takes it.
43
38
  * @returns The card, or `undefined` when nothing renders under that name.
44
39
  */
45
40
  export declare const findCard: (id: string) => CardKind | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../src/cards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AAElF;;;;;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;;;;OAIG;IACH,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;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,IAAI,MAAM,KAAG,QAAQ,GAAG,SAAiD,CAAC"}
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"}
package/build/cards.js CHANGED
@@ -1,4 +1,4 @@
1
- import { gist, pin, stats, themes, topLangs, wakatime } from '@stats-forge/github-stats-forge-core';
1
+ import { 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
4
  const COMMON_OPTIONS = [
@@ -289,7 +289,6 @@ export const cards = CARDS.map((card) => ({
289
289
  options: [...card.options, ...COMMON_OPTIONS],
290
290
  }));
291
291
  /**
292
- * @param id The card's id, as `--card` takes it.
293
292
  * @returns The card, or `undefined` when nothing renders under that name.
294
293
  */
295
294
  export const findCard = (id) => cards.find((card) => card.id === id);
package/build/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { relative, resolve } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
- import { CardConfig } from '@stats-forge/github-stats-forge-core';
5
+ import { CardConfig } from '@stats-forge/github-stats-forge-core/api';
6
6
  import { cards, findCard } from './cards.js';
7
7
  import { askRequired, askSavePath, askToken, navigateOptions, pickCard } from './prompts.js';
8
8
  import { defaultFileName, toQuery } from './query.js';
@@ -45,24 +45,20 @@ const readFlags = () => parseArgs({
45
45
  /**
46
46
  * Renders a card and writes it next to wherever the run was started.
47
47
  *
48
- * @param card The card to render.
49
- * @param query Its params.
50
- * @param config Tokens the fetchers use.
51
- * @param out Where to write it; named after the card when absent.
52
48
  * @returns The file written, or the code that says why nothing was.
53
49
  */
54
50
  const renderAndWrite = async (card, query, config, out) => {
55
51
  const result = await withSpinner(`Rendering the ${card.id} card`, () => card.render(query, config));
56
52
  if (result.status === 'error') {
57
53
  const { code, message, secondaryMessage, param } = result.error;
58
- process.stderr.write([
54
+ process.stderr.write(`${[
59
55
  `Could not render the ${card.id} card.`,
60
56
  ` ${message}${secondaryMessage ? `: ${secondaryMessage}` : ''}`,
61
57
  ` code: ${code}${param ? `, param: ${param}` : ''}`,
62
58
  result.retryable ? ' This one may work on a retry.' : '',
63
59
  ]
64
60
  .filter(Boolean)
65
- .join('\n') + '\n');
61
+ .join('\n')}\n`);
66
62
  return { failed: code };
67
63
  }
68
64
  const file = resolve(process.cwd(), out ?? defaultFileName(card, query));
@@ -73,6 +69,7 @@ const renderAndWrite = async (card, query, config, out) => {
73
69
  };
74
70
  /**
75
71
  * Renders one card and writes it next to wherever the run was started.
72
+ * Sets a non-zero exit code rather than throwing when it could not.
76
73
  *
77
74
  * @returns Nothing; the process exits non-zero when the card could not be rendered.
78
75
  */
@@ -130,14 +127,14 @@ const main = async () => {
130
127
  const config = new CardConfig({ pats: tokens });
131
128
  // `--generate` renders what the file holds and stops: no menu, nothing to answer.
132
129
  if (saved && flags.generate) {
133
- const outcome = await renderAndWrite(card, saved.params, config, flags.out);
130
+ const outcome = await renderAndWrite(card, saved.options, config, flags.out);
134
131
  if ('failed' in outcome) {
135
132
  process.exitCode = 1;
136
133
  }
137
134
  return;
138
135
  }
139
136
  const menu = {
140
- answers: saved ? toAnswers(card, saved.params) : await askRequired(card),
137
+ answers: saved ? toAnswers(card, saved.options) : await askRequired(card),
141
138
  };
142
139
  let savePath = flags.config;
143
140
  /*
@@ -180,13 +177,13 @@ const main = async () => {
180
177
  try {
181
178
  await main();
182
179
  }
183
- catch (err) {
180
+ catch (error) {
184
181
  // A cancelled prompt is a normal way to leave, not a crash.
185
- if (err instanceof Error && err.name === 'ExitPromptError') {
182
+ if (error instanceof Error && error.name === 'ExitPromptError') {
186
183
  process.exitCode = 130;
187
184
  }
188
185
  else {
189
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
186
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
190
187
  process.exitCode = 1;
191
188
  }
192
189
  }
@@ -1,14 +1,12 @@
1
- import type { CardKind, CardOption } from './cards.js';
2
- import type { Answer } from './query.js';
1
+ import type { CardKind, CardOption } from './cards.ts';
2
+ import type { Answer } from './query.ts';
3
3
  /**
4
4
  * @file The navigation itself.
5
5
  *
6
- * A card first, then its required params, then a menu of every other option:
6
+ * A card first, then its required options, then a menu of every other option:
7
7
  * pick one, answer it, and land back on the menu with the answer beside it.
8
8
  */
9
- /**
10
- * @returns The card to render.
11
- */
9
+ /** @returns The card to render. */
12
10
  export declare const pickCard: () => Promise<CardKind>;
13
11
  /** How a trip through the option menu ended. */
14
12
  export type MenuChoice = 'generate' | 'save' | 'quit';
@@ -29,23 +27,18 @@ export interface Menu {
29
27
  * The menu is edited in place, so reopening it after a render keeps every answer
30
28
  * and the cursor exactly where they were.
31
29
  *
32
- * @param card The card being built.
33
- * @param menu Answers so far, and where the cursor sat.
34
- * @param status What happened last time round, shown in the menu's own line.
35
30
  * @returns Whether to render the card or to stop.
36
31
  */
37
32
  export declare const navigateOptions: (card: CardKind, menu: Menu, status?: string) => Promise<MenuChoice>;
38
33
  /**
39
- * Asks for the params the card cannot render without.
34
+ * Asks for the options the card cannot render without.
40
35
  *
41
- * @param card The card being built.
42
36
  * @returns The answers, one per required param.
43
37
  */
44
38
  export declare const askRequired: (card: CardKind) => Promise<Map<string, Answer>>;
45
39
  /**
46
40
  * Asks where to write the options, when no `--config` said.
47
41
  *
48
- * @param suggestion The path offered by default.
49
42
  * @returns The path, or `undefined` when the run changed its mind.
50
43
  */
51
44
  export declare const askSavePath: (suggestion: string) => Promise<string | undefined>;
@@ -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;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAa,OAAO,CAAC,QAAQ,CAI9C,CAAC;AA+BL,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;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC1B,MAAM,QAAQ,EACd,MAAM,IAAI,EACV,SAAS,MAAM,KACd,OAAO,CAAC,UAAU,CAkCpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAU,MAAM,QAAQ,KAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAU,YAAY,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;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"}
package/build/prompts.js CHANGED
@@ -4,21 +4,17 @@ import { describeAnswer } from './query.js';
4
4
  /**
5
5
  * @file The navigation itself.
6
6
  *
7
- * A card first, then its required params, then a menu of every other option:
7
+ * A card first, then its required options, then a menu of every other option:
8
8
  * pick one, answer it, and land back on the menu with the answer beside it.
9
9
  */
10
- /**
11
- * @returns The card to render.
12
- */
13
- export const pickCard = async () => select({
10
+ /** @returns The card to render. */
11
+ export const pickCard = () => select({
14
12
  message: 'Which card?',
15
13
  choices: cards.map((card) => ({ name: card.label, value: card })),
16
14
  });
17
15
  /**
18
16
  * Asks for one option, seeded with whatever it already holds.
19
17
  *
20
- * @param option The option to ask for.
21
- * @param current What it holds now.
22
18
  * @returns The answer, or `undefined` when it was cleared.
23
19
  */
24
20
  const askOption = async (option, current) => {
@@ -45,9 +41,6 @@ const askOption = async (option, current) => {
45
41
  * The menu is edited in place, so reopening it after a render keeps every answer
46
42
  * and the cursor exactly where they were.
47
43
  *
48
- * @param card The card being built.
49
- * @param menu Answers so far, and where the cursor sat.
50
- * @param status What happened last time round, shown in the menu's own line.
51
44
  * @returns Whether to render the card or to stop.
52
45
  */
53
46
  export const navigateOptions = async (card, menu, status) => {
@@ -58,7 +51,8 @@ export const navigateOptions = async (card, menu, status) => {
58
51
  message: status ? `${name} — ${status}` : `${name} — set an option, or generate`,
59
52
  pageSize: 15,
60
53
  // Matched by reference against the values below, so the option objects work.
61
- default: menu.cursor,
54
+ // `default` does not accept an explicit undefined, so an unset cursor omits it.
55
+ ...(menu.cursor !== undefined && { default: menu.cursor }),
62
56
  choices: [
63
57
  { name: 'Generate the card', value: 'generate' },
64
58
  { name: 'Save these options', value: 'save' },
@@ -83,9 +77,8 @@ export const navigateOptions = async (card, menu, status) => {
83
77
  }
84
78
  };
85
79
  /**
86
- * Asks for the params the card cannot render without.
80
+ * Asks for the options the card cannot render without.
87
81
  *
88
- * @param card The card being built.
89
82
  * @returns The answers, one per required param.
90
83
  */
91
84
  export const askRequired = async (card) => {
@@ -101,7 +94,6 @@ export const askRequired = async (card) => {
101
94
  /**
102
95
  * Asks where to write the options, when no `--config` said.
103
96
  *
104
- * @param suggestion The path offered by default.
105
97
  * @returns The path, or `undefined` when the run changed its mind.
106
98
  */
107
99
  export const askSavePath = async (suggestion) => {
package/build/query.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CardKind, CardOption } from './cards.js';
1
+ import type { CardKind, CardOption } from './cards.ts';
2
2
  /**
3
3
  * @file Answers in, query params out.
4
4
  *
@@ -8,29 +8,23 @@ import type { CardKind, CardOption } from './cards.js';
8
8
  /** What a prompt answered, before it becomes a query param. */
9
9
  export type Answer = string | number | boolean | Array<string> | undefined;
10
10
  /**
11
- * @param value What the prompt returned.
12
11
  * @returns The query string form, or `undefined` when there is nothing to send.
13
12
  */
14
13
  export declare const toParam: (value: Answer) => string | undefined;
15
14
  /**
16
- * @param answers What each option was answered with.
17
15
  * @returns The query the card handler is called with.
18
16
  */
19
17
  export declare const toQuery: (answers: ReadonlyMap<string, Answer>) => Record<string, string>;
20
18
  /**
21
19
  * How an answer reads back in the option menu.
22
20
  *
23
- * @param option The option it answers.
24
- * @param value What it was answered with.
25
21
  * @returns The value as the menu shows it.
26
22
  */
27
23
  export declare const describeAnswer: (option: CardOption, value: Answer) => string;
28
24
  /**
29
- * The file a card is written to when `--out` is not given.
25
+ * The `.svg` file a card is written to when `--out` is not given.
30
26
  * Named after the card and whoever it is about, so a directory of them stays readable.
31
27
  *
32
- * @param card The card being rendered.
33
- * @param query The answers it was rendered from.
34
28
  * @returns A file name, ending in `.svg`.
35
29
  */
36
30
  export declare const defaultFileName: (card: CardKind, query: Record<string, string>) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;GAKG;AAEH,+DAA+D;AAC/D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;AAE3E;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,MAAM,KAAG,MAAM,GAAG,SAQhD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CASnF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,UAAU,EAAE,OAAO,MAAM,KAAG,MAMlE,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,QAAQ,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAK/E,CAAC"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;;GAKG;AAEH,+DAA+D;AAC/D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;AAE3E;;GAEG;AACH,eAAO,MAAM,OAAO,UAAW,MAAM,KAAG,MAAM,GAAG,SAQhD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,YAAa,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CASnF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,WAAY,UAAU,SAAS,MAAM,KAAG,MAMlE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,SAAU,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAK/E,CAAC"}
package/build/query.js CHANGED
@@ -1,5 +1,4 @@
1
1
  /**
2
- * @param value What the prompt returned.
3
2
  * @returns The query string form, or `undefined` when there is nothing to send.
4
3
  */
5
4
  export const toParam = (value) => {
@@ -12,7 +11,6 @@ export const toParam = (value) => {
12
11
  return String(value);
13
12
  };
14
13
  /**
15
- * @param answers What each option was answered with.
16
14
  * @returns The query the card handler is called with.
17
15
  */
18
16
  export const toQuery = (answers) => {
@@ -28,8 +26,6 @@ export const toQuery = (answers) => {
28
26
  /**
29
27
  * How an answer reads back in the option menu.
30
28
  *
31
- * @param option The option it answers.
32
- * @param value What it was answered with.
33
29
  * @returns The value as the menu shows it.
34
30
  */
35
31
  export const describeAnswer = (option, value) => {
@@ -40,16 +36,14 @@ export const describeAnswer = (option, value) => {
40
36
  return option.kind === 'boolean' ? (value === true ? 'yes' : 'no') : param;
41
37
  };
42
38
  /**
43
- * The file a card is written to when `--out` is not given.
39
+ * The `.svg` file a card is written to when `--out` is not given.
44
40
  * Named after the card and whoever it is about, so a directory of them stays readable.
45
41
  *
46
- * @param card The card being rendered.
47
- * @param query The answers it was rendered from.
48
42
  * @returns A file name, ending in `.svg`.
49
43
  */
50
44
  export const defaultFileName = (card, query) => {
51
45
  const subject = query['username'] ?? query['id'] ?? 'card';
52
- const repo = query['repo'];
46
+ const { repo } = query;
53
47
  const parts = [card.id, subject, repo].filter(Boolean).join('-');
54
- return `${parts.replace(/[^\w.-]/g, '-')}.svg`;
48
+ return `${parts.replaceAll(/[^\w.-]/g, '-')}.svg`;
55
49
  };
@@ -1,39 +1,33 @@
1
- import type { CardKind } from './cards.js';
2
- import type { Answer } from './query.js';
1
+ import type { CardKind } from './cards.ts';
2
+ import type { Answer } from './query.ts';
3
3
  /**
4
- * @param path File to look for, relative to the working directory.
5
4
  * @returns Whether there is something there to load.
6
5
  */
7
6
  export declare const savedCardExists: (path: string) => boolean;
8
7
  /**
9
8
  * Reads a card back off disk.
10
9
  *
11
- * @param path File to read, relative to the working directory.
12
- * @returns The card it names, and its params.
13
10
  * @throws {Error} When the file is not a card this version can render.
11
+ *
12
+ * @returns The card it names, and its options.
14
13
  */
15
14
  export declare const readSavedCard: (path: string) => Promise<{
16
15
  card: CardKind;
17
- params: Record<string, string>;
16
+ options: Record<string, string>;
18
17
  }>;
19
18
  /**
20
19
  * Writes a card down, so the same one can be rendered again later.
21
20
  *
22
- * @param path File to write, relative to the working directory.
23
- * @param card The card being rendered.
24
- * @param params Its params, as they reach the endpoint.
25
21
  * @returns The path written to.
26
22
  */
27
- export declare const writeSavedCard: (path: string, card: CardKind, params: Record<string, string>) => Promise<string>;
23
+ export declare const writeSavedCard: (path: string, card: CardKind, options: Record<string, string>) => Promise<string>;
28
24
  /**
29
- * Turns saved params back into answers the menu can show and edit.
25
+ * Turns saved options back into answers the menu can show and edit.
30
26
  *
31
27
  * Everything on a query string is a string;
32
28
  * a boolean option becomes one again so its prompt opens on the right answer.
33
29
  *
34
- * @param card The card the params belong to.
35
- * @param params The saved params.
36
30
  * @returns The answers, ready for the menu.
37
31
  */
38
- export declare const toAnswers: (card: CardKind, params: Record<string, string>) => Map<string, Answer>;
32
+ export declare const toAnswers: (card: CardKind, options: Record<string, string>) => Map<string, Answer>;
39
33
  //# sourceMappingURL=saved-card.d.ts.map
@@ -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;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,OAAmD,CAAC;AAEnG;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,KACX,OAAO,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAyB5D,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EACZ,MAAM,QAAQ,EACd,QAAQ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,MAAM,CAKhB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,QAAQ,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAW5F,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;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,SAAU,QAAQ,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAW7F,CAAC"}
@@ -3,16 +3,15 @@ import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
4
  import { findCard } from './cards.js';
5
5
  /**
6
- * @param path File to look for, relative to the working directory.
7
6
  * @returns Whether there is something there to load.
8
7
  */
9
8
  export const savedCardExists = (path) => existsSync(resolve(process.cwd(), path));
10
9
  /**
11
10
  * Reads a card back off disk.
12
11
  *
13
- * @param path File to read, relative to the working directory.
14
- * @returns The card it names, and its params.
15
12
  * @throws {Error} When the file is not a card this version can render.
13
+ *
14
+ * @returns The card it names, and its options.
16
15
  */
17
16
  export const readSavedCard = async (path) => {
18
17
  const file = resolve(process.cwd(), path);
@@ -20,48 +19,43 @@ export const readSavedCard = async (path) => {
20
19
  try {
21
20
  parsed = JSON.parse(await readFile(file, 'utf8'));
22
21
  }
23
- catch (err) {
24
- throw new Error(`${file} is not readable as JSON`, { cause: err });
22
+ catch (error) {
23
+ throw new Error(`${file} is not readable as JSON`, { cause: error });
25
24
  }
26
25
  if (typeof parsed !== 'object' || parsed === null) {
27
26
  throw new Error(`${file} does not hold a saved card`);
28
27
  }
29
- const { card: id, params } = parsed;
28
+ const { card: id, options } = parsed;
30
29
  const card = typeof id === 'string' ? findCard(id) : undefined;
31
30
  if (!card) {
32
31
  throw new Error(`${file} names no card this version renders: ${id ?? '(nothing)'}`);
33
32
  }
34
- // A param that is not a string could not have come off a query string.
35
- const entries = Object.entries(params ?? {}).filter((entry) => typeof entry[1] === 'string');
36
- return { card, params: Object.fromEntries(entries) };
33
+ // An option that is not a string could not have come off a query string.
34
+ const entries = Object.entries(options ?? {}).filter((entry) => typeof entry[1] === 'string');
35
+ return { card, options: Object.fromEntries(entries) };
37
36
  };
38
37
  /**
39
38
  * Writes a card down, so the same one can be rendered again later.
40
39
  *
41
- * @param path File to write, relative to the working directory.
42
- * @param card The card being rendered.
43
- * @param params Its params, as they reach the endpoint.
44
40
  * @returns The path written to.
45
41
  */
46
- export const writeSavedCard = async (path, card, params) => {
42
+ export const writeSavedCard = async (path, card, options) => {
47
43
  const file = resolve(process.cwd(), path);
48
- const saved = { card: card.id, params };
44
+ const saved = { card: card.id, options };
49
45
  await writeFile(file, `${JSON.stringify(saved, null, 2)}\n`, 'utf8');
50
46
  return file;
51
47
  };
52
48
  /**
53
- * Turns saved params back into answers the menu can show and edit.
49
+ * Turns saved options back into answers the menu can show and edit.
54
50
  *
55
51
  * Everything on a query string is a string;
56
52
  * a boolean option becomes one again so its prompt opens on the right answer.
57
53
  *
58
- * @param card The card the params belong to.
59
- * @param params The saved params.
60
54
  * @returns The answers, ready for the menu.
61
55
  */
62
- export const toAnswers = (card, params) => {
56
+ export const toAnswers = (card, options) => {
63
57
  const kinds = new Map([...card.required, ...card.options].map((option) => [option.name, option.kind]));
64
- return new Map(Object.entries(params).map(([name, value]) => [
58
+ return new Map(Object.entries(options).map(([name, value]) => [
65
59
  name,
66
60
  kinds.get(name) === 'boolean' ? value === 'true' : value,
67
61
  ]));
@@ -15,9 +15,6 @@ export interface SpinnerStream {
15
15
  * Written to stderr, so stdout carries only the result.
16
16
  * Without a TTY — a pipe, a CI log — the label is printed once and nothing animates.
17
17
  *
18
- * @param label What the wait is for.
19
- * @param work The wait itself.
20
- * @param stream Where the spinner is drawn.
21
18
  * @returns Whatever `work` answered with.
22
19
  */
23
20
  export declare const withSpinner: <T>(label: string, work: () => Promise<T>, stream?: SpinnerStream) => Promise<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../src/spinner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GAAU,CAAC,EACjC,OAAO,MAAM,EACb,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,SAAQ,aAA8B,KACrC,OAAO,CAAC,CAAC,CAoBX,CAAC"}
1
+ {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../src/spinner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAU,CAAC,SAC1B,MAAM,QACP,MAAM,OAAO,CAAC,CAAC,CAAC,WACd,aAAa,KACpB,OAAO,CAAC,CAAC,CAoBX,CAAC"}
package/build/spinner.js CHANGED
@@ -7,19 +7,16 @@
7
7
  /** Braille frames: one cell wide, so the line never reflows. */
8
8
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
9
9
  const INTERVAL_MS = 80;
10
- const HIDE_CURSOR = '\u001b[?25l';
11
- const SHOW_CURSOR = '\u001b[?25h';
10
+ const HIDE_CURSOR = '\u001B[?25l';
11
+ const SHOW_CURSOR = '\u001B[?25h';
12
12
  /** Return to the start of the line and wipe what was on it. */
13
- const CLEAR_LINE = '\r\u001b[K';
13
+ const CLEAR_LINE = '\r\u001B[K';
14
14
  /**
15
15
  * Runs `work`, spinning until it settles.
16
16
  *
17
17
  * Written to stderr, so stdout carries only the result.
18
18
  * Without a TTY — a pipe, a CI log — the label is printed once and nothing animates.
19
19
  *
20
- * @param label What the wait is for.
21
- * @param work The wait itself.
22
- * @param stream Where the spinner is drawn.
23
20
  * @returns Whatever `work` answered with.
24
21
  */
25
22
  export const withSpinner = async (label, work, stream = process.stderr) => {
package/build/tokens.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PersonalAccessToken } from '@stats-forge/github-stats-forge-core';
1
+ import type { PersonalAccessToken } from '@stats-forge/github-stats-forge-core/api';
2
2
  /**
3
3
  * @file Where the GitHub token comes from.
4
4
  *
@@ -11,24 +11,21 @@ export declare const DEFAULT_ENV_FILE = ".env";
11
11
  /**
12
12
  * Loads an env file into `process.env`, the way `node --env-file` would.
13
13
  *
14
- * @param path File to load, relative to the working directory.
15
- * @param required Whether a missing file is an error.
16
- * @returns Whether anything was loaded.
17
14
  * @throws {Error} When `required` and the file is not there.
15
+ *
16
+ * @returns Whether anything was loaded.
18
17
  */
19
18
  export declare const loadEnvFile: (path: string, required: boolean) => boolean;
20
19
  /**
21
- * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads.
20
+ * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads,
21
+ * in name order and skipping any that are empty.
22
22
  *
23
- * @param env Environment to read.
24
23
  * @returns The tokens, in name order, skipping any that are empty.
25
24
  */
26
25
  export declare const tokensFromEnv: (env: Record<string, string | undefined>) => Array<PersonalAccessToken>;
27
26
  /**
28
27
  * The tokens a run will use.
29
28
  *
30
- * @param flags Tokens passed as `--pat`, which win over the environment.
31
- * @param env Environment to read, once any env file has been loaded into it.
32
29
  * @returns The tokens, empty when the run has none yet.
33
30
  */
34
31
  export declare const resolveTokens: (flags: ReadonlyArray<string>, env: Record<string, string | undefined>) => Array<PersonalAccessToken>;
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAEhF;;;;;;GAMG;AAEH,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAEvC;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,UAAU,OAAO,KAAG,OAU7D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KACtC,KAAK,CAAC,mBAAmB,CAOtB,CAAC;AAEP;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,CAAC,MAAM,CAAC,EAC5B,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KACtC,KAAK,CAAC,mBAAmB,CAO3B,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAEpF;;;;;;GAMG;AAEH,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,SAAU,MAAM,YAAY,OAAO,KAAG,OAU7D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QACnB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KACtC,KAAK,CAAC,mBAAmB,CAOtB,CAAC;AAEP;;;;GAIG;AACH,eAAO,MAAM,aAAa,UACjB,aAAa,CAAC,MAAM,CAAC,OACvB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KACtC,KAAK,CAAC,mBAAmB,CAO3B,CAAC"}
package/build/tokens.js CHANGED
@@ -12,10 +12,9 @@ export const DEFAULT_ENV_FILE = '.env';
12
12
  /**
13
13
  * Loads an env file into `process.env`, the way `node --env-file` would.
14
14
  *
15
- * @param path File to load, relative to the working directory.
16
- * @param required Whether a missing file is an error.
17
- * @returns Whether anything was loaded.
18
15
  * @throws {Error} When `required` and the file is not there.
16
+ *
17
+ * @returns Whether anything was loaded.
19
18
  */
20
19
  export const loadEnvFile = (path, required) => {
21
20
  const absolute = resolve(process.cwd(), path);
@@ -29,14 +28,14 @@ export const loadEnvFile = (path, required) => {
29
28
  return true;
30
29
  };
31
30
  /**
32
- * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads.
31
+ * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads,
32
+ * in name order and skipping any that are empty.
33
33
  *
34
- * @param env Environment to read.
35
34
  * @returns The tokens, in name order, skipping any that are empty.
36
35
  */
37
36
  export const tokensFromEnv = (env) => Object.keys(env)
38
37
  .filter((name) => /^PAT_\d+$/.test(name))
39
- .sort()
38
+ .toSorted()
40
39
  .flatMap((name) => {
41
40
  const value = env[name];
42
41
  return value ? [{ name, value }] : [];
@@ -44,8 +43,6 @@ export const tokensFromEnv = (env) => Object.keys(env)
44
43
  /**
45
44
  * The tokens a run will use.
46
45
  *
47
- * @param flags Tokens passed as `--pat`, which win over the environment.
48
- * @param env Environment to read, once any env file has been loaded into it.
49
46
  * @returns The tokens, empty when the run has none yet.
50
47
  */
51
48
  export const resolveTokens = (flags, env) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stats-forge/github-stats-forge-cli",
3
- "version": "0.0.2",
3
+ "version": "0.1.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",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/stats-forge/github-stats-forge.git",
22
+ "url": "git+https://github.com/stats-forge/github-stats-forge.git",
23
23
  "directory": "packages/cli"
24
24
  },
25
25
  "bin": {
@@ -40,20 +40,20 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@inquirer/prompts": "8.6.0",
44
- "@stats-forge/github-stats-forge-core": "^0.0.2"
43
+ "@inquirer/prompts": "8.7.0",
44
+ "@stats-forge/github-stats-forge-core": "^0.1.0"
45
45
  },
46
46
  "devDependencies": {
47
- "vitest": "4.1.10"
47
+ "vitest": "4.1.11"
48
48
  },
49
49
  "engines": {
50
- "node": ">=24"
50
+ "node": "^24 || >=26"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsc -p tsconfig.build.json",
54
- "dev": "pnpm run build && node build/index.js",
54
+ "lint:publish": "attw --pack . --profile node16 --ignore-rules cjs-resolves-to-esm && publint --strict",
55
+ "dev": "node --conditions=@stats/source src/index.ts",
55
56
  "test": "vitest",
56
- "lint": "eslint",
57
57
  "typecheck": "tsc -p tsconfig.typecheck.json"
58
58
  }
59
59
  }
package/src/cards.ts CHANGED
@@ -1,5 +1,12 @@
1
- import { gist, pin, stats, themes, topLangs, wakatime } from '@stats-forge/github-stats-forge-core';
2
- import type { ApiResult, CardConfig } from '@stats-forge/github-stats-forge-core';
1
+ import {
2
+ gist,
3
+ pin,
4
+ stats,
5
+ themes,
6
+ topLangs,
7
+ wakatime,
8
+ } from '@stats-forge/github-stats-forge-core/api';
9
+ import type { ApiResult, CardConfig } from '@stats-forge/github-stats-forge-core/api';
3
10
 
4
11
  /**
5
12
  * @file What each card accepts, in the order the prompts walk it.
@@ -34,11 +41,7 @@ export interface CardKind {
34
41
  required: ReadonlyArray<CardOption>;
35
42
  /** Everything else, navigable in any order. */
36
43
  options: ReadonlyArray<CardOption>;
37
- /**
38
- * @param query The answers, as a query string would carry them.
39
- * @param config Tokens the fetchers use.
40
- * @returns The rendered card, or the rendered error.
41
- */
44
+ /** @returns The rendered card, or the rendered error. */
42
45
  render: (query: Record<string, string>, config: CardConfig) => Promise<ApiResult>;
43
46
  }
44
47
 
@@ -337,7 +340,6 @@ export const cards: ReadonlyArray<CardKind> = CARDS.map((card) => ({
337
340
  }));
338
341
 
339
342
  /**
340
- * @param id The card's id, as `--card` takes it.
341
343
  * @returns The card, or `undefined` when nothing renders under that name.
342
344
  */
343
345
  export const findCard = (id: string): CardKind | undefined => cards.find((card) => card.id === id);
package/src/index.ts CHANGED
@@ -3,16 +3,16 @@ import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { relative, resolve } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
 
6
- import { CardConfig } from '@stats-forge/github-stats-forge-core';
6
+ import { CardConfig } from '@stats-forge/github-stats-forge-core/api';
7
7
 
8
- import type { CardKind } from './cards.js';
9
- import { cards, findCard } from './cards.js';
10
- import type { Menu } from './prompts.js';
11
- import { askRequired, askSavePath, askToken, navigateOptions, pickCard } from './prompts.js';
12
- import { defaultFileName, toQuery } from './query.js';
13
- import { readSavedCard, savedCardExists, toAnswers, writeSavedCard } from './saved-card.js';
14
- import { withSpinner } from './spinner.js';
15
- import { DEFAULT_ENV_FILE, loadEnvFile, resolveTokens } from './tokens.js';
8
+ import type { CardKind } from './cards.ts';
9
+ import { cards, findCard } from './cards.ts';
10
+ import type { Menu } from './prompts.ts';
11
+ import { askRequired, askSavePath, askToken, navigateOptions, pickCard } from './prompts.ts';
12
+ import { defaultFileName, toQuery } from './query.ts';
13
+ import { readSavedCard, savedCardExists, toAnswers, writeSavedCard } from './saved-card.ts';
14
+ import { withSpinner } from './spinner.ts';
15
+ import { DEFAULT_ENV_FILE, loadEnvFile, resolveTokens } from './tokens.ts';
16
16
 
17
17
  const HELP = `github-stats-forge — render a GitHub stats card to a local SVG
18
18
 
@@ -32,10 +32,22 @@ Options
32
32
  The token can also come from PAT_1 in the environment, or be typed when asked.
33
33
  `;
34
34
 
35
+ /** What the command line can carry. */
36
+ interface Flags {
37
+ card?: string;
38
+ out?: string;
39
+ config?: string;
40
+ generate: boolean;
41
+ pat: Array<string>;
42
+ 'env-file'?: string;
43
+ help: boolean;
44
+ version: boolean;
45
+ }
46
+
35
47
  /**
36
48
  * @returns The flags this run was given.
37
49
  */
38
- const readFlags = () =>
50
+ const readFlags = (): Flags =>
39
51
  parseArgs({
40
52
  options: {
41
53
  card: { type: 'string', short: 'c' },
@@ -53,10 +65,6 @@ const readFlags = () =>
53
65
  /**
54
66
  * Renders a card and writes it next to wherever the run was started.
55
67
  *
56
- * @param card The card to render.
57
- * @param query Its params.
58
- * @param config Tokens the fetchers use.
59
- * @param out Where to write it; named after the card when absent.
60
68
  * @returns The file written, or the code that says why nothing was.
61
69
  */
62
70
  const renderAndWrite = async (
@@ -72,14 +80,14 @@ const renderAndWrite = async (
72
80
  if (result.status === 'error') {
73
81
  const { code, message, secondaryMessage, param } = result.error;
74
82
  process.stderr.write(
75
- [
83
+ `${[
76
84
  `Could not render the ${card.id} card.`,
77
85
  ` ${message}${secondaryMessage ? `: ${secondaryMessage}` : ''}`,
78
86
  ` code: ${code}${param ? `, param: ${param}` : ''}`,
79
87
  result.retryable ? ' This one may work on a retry.' : '',
80
88
  ]
81
89
  .filter(Boolean)
82
- .join('\n') + '\n',
90
+ .join('\n')}\n`,
83
91
  );
84
92
  return { failed: code };
85
93
  }
@@ -93,6 +101,7 @@ const renderAndWrite = async (
93
101
 
94
102
  /**
95
103
  * Renders one card and writes it next to wherever the run was started.
104
+ * Sets a non-zero exit code rather than throwing when it could not.
96
105
  *
97
106
  * @returns Nothing; the process exits non-zero when the card could not be rendered.
98
107
  */
@@ -173,7 +182,7 @@ const main = async (): Promise<void> => {
173
182
 
174
183
  // `--generate` renders what the file holds and stops: no menu, nothing to answer.
175
184
  if (saved && flags.generate) {
176
- const outcome = await renderAndWrite(card, saved.params, config, flags.out);
185
+ const outcome = await renderAndWrite(card, saved.options, config, flags.out);
177
186
  if ('failed' in outcome) {
178
187
  process.exitCode = 1;
179
188
  }
@@ -181,7 +190,7 @@ const main = async (): Promise<void> => {
181
190
  }
182
191
 
183
192
  const menu: Menu = {
184
- answers: saved ? toAnswers(card, saved.params) : await askRequired(card),
193
+ answers: saved ? toAnswers(card, saved.options) : await askRequired(card),
185
194
  };
186
195
  let savePath = flags.config;
187
196
 
@@ -232,12 +241,12 @@ const main = async (): Promise<void> => {
232
241
 
233
242
  try {
234
243
  await main();
235
- } catch (err) {
244
+ } catch (error) {
236
245
  // A cancelled prompt is a normal way to leave, not a crash.
237
- if (err instanceof Error && err.name === 'ExitPromptError') {
246
+ if (error instanceof Error && error.name === 'ExitPromptError') {
238
247
  process.exitCode = 130;
239
248
  } else {
240
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
249
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
241
250
  process.exitCode = 1;
242
251
  }
243
252
  }
package/src/prompts.ts CHANGED
@@ -1,21 +1,19 @@
1
1
  import { confirm, input, password, select } from '@inquirer/prompts';
2
2
 
3
- import type { CardKind, CardOption } from './cards.js';
4
- import { cards } from './cards.js';
5
- import type { Answer } from './query.js';
6
- import { describeAnswer } from './query.js';
3
+ import type { CardKind, CardOption } from './cards.ts';
4
+ import { cards } from './cards.ts';
5
+ import type { Answer } from './query.ts';
6
+ import { describeAnswer } from './query.ts';
7
7
 
8
8
  /**
9
9
  * @file The navigation itself.
10
10
  *
11
- * A card first, then its required params, then a menu of every other option:
11
+ * A card first, then its required options, then a menu of every other option:
12
12
  * pick one, answer it, and land back on the menu with the answer beside it.
13
13
  */
14
14
 
15
- /**
16
- * @returns The card to render.
17
- */
18
- export const pickCard = async (): Promise<CardKind> =>
15
+ /** @returns The card to render. */
16
+ export const pickCard = (): Promise<CardKind> =>
19
17
  select({
20
18
  message: 'Which card?',
21
19
  choices: cards.map((card) => ({ name: card.label, value: card })),
@@ -24,8 +22,6 @@ export const pickCard = async (): Promise<CardKind> =>
24
22
  /**
25
23
  * Asks for one option, seeded with whatever it already holds.
26
24
  *
27
- * @param option The option to ask for.
28
- * @param current What it holds now.
29
25
  * @returns The answer, or `undefined` when it was cleared.
30
26
  */
31
27
  const askOption = async (option: CardOption, current: Answer): Promise<Answer> => {
@@ -71,9 +67,6 @@ export interface Menu {
71
67
  * The menu is edited in place, so reopening it after a render keeps every answer
72
68
  * and the cursor exactly where they were.
73
69
  *
74
- * @param card The card being built.
75
- * @param menu Answers so far, and where the cursor sat.
76
- * @param status What happened last time round, shown in the menu's own line.
77
70
  * @returns Whether to render the card or to stop.
78
71
  */
79
72
  export const navigateOptions = async (
@@ -89,7 +82,8 @@ export const navigateOptions = async (
89
82
  message: status ? `${name} — ${status}` : `${name} — set an option, or generate`,
90
83
  pageSize: 15,
91
84
  // Matched by reference against the values below, so the option objects work.
92
- default: menu.cursor,
85
+ // `default` does not accept an explicit undefined, so an unset cursor omits it.
86
+ ...(menu.cursor !== undefined && { default: menu.cursor }),
93
87
  choices: [
94
88
  { name: 'Generate the card', value: 'generate' as const },
95
89
  { name: 'Save these options', value: 'save' as const },
@@ -117,9 +111,8 @@ export const navigateOptions = async (
117
111
  };
118
112
 
119
113
  /**
120
- * Asks for the params the card cannot render without.
114
+ * Asks for the options the card cannot render without.
121
115
  *
122
- * @param card The card being built.
123
116
  * @returns The answers, one per required param.
124
117
  */
125
118
  export const askRequired = async (card: CardKind): Promise<Map<string, Answer>> => {
@@ -139,7 +132,6 @@ export const askRequired = async (card: CardKind): Promise<Map<string, Answer>>
139
132
  /**
140
133
  * Asks where to write the options, when no `--config` said.
141
134
  *
142
- * @param suggestion The path offered by default.
143
135
  * @returns The path, or `undefined` when the run changed its mind.
144
136
  */
145
137
  export const askSavePath = async (suggestion: string): Promise<string | undefined> => {
package/src/query.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CardKind, CardOption } from './cards.js';
1
+ import type { CardKind, CardOption } from './cards.ts';
2
2
 
3
3
  /**
4
4
  * @file Answers in, query params out.
@@ -11,7 +11,6 @@ import type { CardKind, CardOption } from './cards.js';
11
11
  export type Answer = string | number | boolean | Array<string> | undefined;
12
12
 
13
13
  /**
14
- * @param value What the prompt returned.
15
14
  * @returns The query string form, or `undefined` when there is nothing to send.
16
15
  */
17
16
  export const toParam = (value: Answer): string | undefined => {
@@ -25,7 +24,6 @@ export const toParam = (value: Answer): string | undefined => {
25
24
  };
26
25
 
27
26
  /**
28
- * @param answers What each option was answered with.
29
27
  * @returns The query the card handler is called with.
30
28
  */
31
29
  export const toQuery = (answers: ReadonlyMap<string, Answer>): Record<string, string> => {
@@ -42,8 +40,6 @@ export const toQuery = (answers: ReadonlyMap<string, Answer>): Record<string, st
42
40
  /**
43
41
  * How an answer reads back in the option menu.
44
42
  *
45
- * @param option The option it answers.
46
- * @param value What it was answered with.
47
43
  * @returns The value as the menu shows it.
48
44
  */
49
45
  export const describeAnswer = (option: CardOption, value: Answer): string => {
@@ -55,16 +51,14 @@ export const describeAnswer = (option: CardOption, value: Answer): string => {
55
51
  };
56
52
 
57
53
  /**
58
- * The file a card is written to when `--out` is not given.
54
+ * The `.svg` file a card is written to when `--out` is not given.
59
55
  * Named after the card and whoever it is about, so a directory of them stays readable.
60
56
  *
61
- * @param card The card being rendered.
62
- * @param query The answers it was rendered from.
63
57
  * @returns A file name, ending in `.svg`.
64
58
  */
65
59
  export const defaultFileName = (card: CardKind, query: Record<string, string>): string => {
66
60
  const subject = query['username'] ?? query['id'] ?? 'card';
67
- const repo = query['repo'];
61
+ const { repo } = query;
68
62
  const parts = [card.id, subject, repo].filter(Boolean).join('-');
69
- return `${parts.replace(/[^\w.-]/g, '-')}.svg`;
63
+ return `${parts.replaceAll(/[^\w.-]/g, '-')}.svg`;
70
64
  };
package/src/saved-card.ts CHANGED
@@ -2,14 +2,14 @@ import { existsSync } from 'node:fs';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
4
 
5
- import type { CardKind } from './cards.js';
6
- import { findCard } from './cards.js';
7
- import type { Answer } from './query.js';
5
+ import type { CardKind } from './cards.ts';
6
+ import { findCard } from './cards.ts';
7
+ import type { Answer } from './query.ts';
8
8
 
9
9
  /**
10
10
  * @file A card, written down.
11
11
  *
12
- * The file holds what a query string would hold — the card and its params as
12
+ * The file holds what a query string would hold — the card and its options as
13
13
  * strings — so it reads like the URL it stands for, and can be edited by hand.
14
14
  */
15
15
 
@@ -17,12 +17,11 @@ import type { Answer } from './query.js';
17
17
  interface SavedCard {
18
18
  /** Which card: `stats`, `top-langs`, `pin`, `gist`, `wakatime`. */
19
19
  card: string;
20
- /** The params, exactly as they reach the endpoint. */
21
- params: Record<string, string>;
20
+ /** The options, exactly as they reach the endpoint. */
21
+ options: Record<string, string>;
22
22
  }
23
23
 
24
24
  /**
25
- * @param path File to look for, relative to the working directory.
26
25
  * @returns Whether there is something there to load.
27
26
  */
28
27
  export const savedCardExists = (path: string): boolean => existsSync(resolve(process.cwd(), path));
@@ -30,75 +29,70 @@ export const savedCardExists = (path: string): boolean => existsSync(resolve(pro
30
29
  /**
31
30
  * Reads a card back off disk.
32
31
  *
33
- * @param path File to read, relative to the working directory.
34
- * @returns The card it names, and its params.
35
32
  * @throws {Error} When the file is not a card this version can render.
33
+ *
34
+ * @returns The card it names, and its options.
36
35
  */
37
36
  export const readSavedCard = async (
38
37
  path: string,
39
- ): Promise<{ card: CardKind; params: Record<string, string> }> => {
38
+ ): Promise<{ card: CardKind; options: Record<string, string> }> => {
40
39
  const file = resolve(process.cwd(), path);
41
40
  let parsed: unknown;
42
41
  try {
43
42
  parsed = JSON.parse(await readFile(file, 'utf8'));
44
- } catch (err) {
45
- throw new Error(`${file} is not readable as JSON`, { cause: err });
43
+ } catch (error) {
44
+ throw new Error(`${file} is not readable as JSON`, { cause: error });
46
45
  }
47
46
 
48
47
  if (typeof parsed !== 'object' || parsed === null) {
49
48
  throw new Error(`${file} does not hold a saved card`);
50
49
  }
51
50
 
52
- const { card: id, params } = parsed as Partial<SavedCard>;
51
+ const { card: id, options } = parsed as Partial<SavedCard>;
53
52
  const card = typeof id === 'string' ? findCard(id) : undefined;
54
53
  if (!card) {
55
54
  throw new Error(`${file} names no card this version renders: ${id ?? '(nothing)'}`);
56
55
  }
57
56
 
58
- // A param that is not a string could not have come off a query string.
59
- const entries = Object.entries(params ?? {}).filter(
57
+ // An option that is not a string could not have come off a query string.
58
+ const entries = Object.entries(options ?? {}).filter(
60
59
  (entry): entry is [string, string] => typeof entry[1] === 'string',
61
60
  );
62
61
 
63
- return { card, params: Object.fromEntries(entries) };
62
+ return { card, options: Object.fromEntries(entries) };
64
63
  };
65
64
 
66
65
  /**
67
66
  * Writes a card down, so the same one can be rendered again later.
68
67
  *
69
- * @param path File to write, relative to the working directory.
70
- * @param card The card being rendered.
71
- * @param params Its params, as they reach the endpoint.
72
68
  * @returns The path written to.
73
69
  */
74
70
  export const writeSavedCard = async (
75
71
  path: string,
76
72
  card: CardKind,
77
- params: Record<string, string>,
73
+ options: Record<string, string>,
78
74
  ): Promise<string> => {
79
75
  const file = resolve(process.cwd(), path);
80
- const saved: SavedCard = { card: card.id, params };
76
+ const saved: SavedCard = { card: card.id, options };
81
77
  await writeFile(file, `${JSON.stringify(saved, null, 2)}\n`, 'utf8');
82
78
  return file;
83
79
  };
84
80
 
85
81
  /**
86
- * Turns saved params back into answers the menu can show and edit.
82
+ * Turns saved options back into answers the menu can show and edit.
87
83
  *
88
84
  * Everything on a query string is a string;
89
85
  * a boolean option becomes one again so its prompt opens on the right answer.
90
86
  *
91
- * @param card The card the params belong to.
92
- * @param params The saved params.
93
87
  * @returns The answers, ready for the menu.
94
88
  */
95
- export const toAnswers = (card: CardKind, params: Record<string, string>): Map<string, Answer> => {
89
+ export const toAnswers = (card: CardKind, options: Record<string, string>): Map<string, Answer> => {
96
90
  const kinds = new Map(
97
91
  [...card.required, ...card.options].map((option) => [option.name, option.kind]),
98
92
  );
99
93
 
100
94
  return new Map(
101
- Object.entries(params).map(([name, value]) => [
95
+ Object.entries(options).map(([name, value]) => [
102
96
  name,
103
97
  kinds.get(name) === 'boolean' ? value === 'true' : value,
104
98
  ]),
package/src/spinner.ts CHANGED
@@ -9,10 +9,10 @@
9
9
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10
10
  const INTERVAL_MS = 80;
11
11
 
12
- const HIDE_CURSOR = '\u001b[?25l';
13
- const SHOW_CURSOR = '\u001b[?25h';
12
+ const HIDE_CURSOR = '\u001B[?25l';
13
+ const SHOW_CURSOR = '\u001B[?25h';
14
14
  /** Return to the start of the line and wipe what was on it. */
15
- const CLEAR_LINE = '\r\u001b[K';
15
+ const CLEAR_LINE = '\r\u001B[K';
16
16
 
17
17
  /** What the spinner writes to; `process.stderr`, or a fake in a test. */
18
18
  export interface SpinnerStream {
@@ -26,9 +26,6 @@ export interface SpinnerStream {
26
26
  * Written to stderr, so stdout carries only the result.
27
27
  * Without a TTY — a pipe, a CI log — the label is printed once and nothing animates.
28
28
  *
29
- * @param label What the wait is for.
30
- * @param work The wait itself.
31
- * @param stream Where the spinner is drawn.
32
29
  * @returns Whatever `work` answered with.
33
30
  */
34
31
  export const withSpinner = async <T>(
package/src/tokens.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
- import type { PersonalAccessToken } from '@stats-forge/github-stats-forge-core';
4
+ import type { PersonalAccessToken } from '@stats-forge/github-stats-forge-core/api';
5
5
 
6
6
  /**
7
7
  * @file Where the GitHub token comes from.
@@ -17,10 +17,9 @@ export const DEFAULT_ENV_FILE = '.env';
17
17
  /**
18
18
  * Loads an env file into `process.env`, the way `node --env-file` would.
19
19
  *
20
- * @param path File to load, relative to the working directory.
21
- * @param required Whether a missing file is an error.
22
- * @returns Whether anything was loaded.
23
20
  * @throws {Error} When `required` and the file is not there.
21
+ *
22
+ * @returns Whether anything was loaded.
24
23
  */
25
24
  export const loadEnvFile = (path: string, required: boolean): boolean => {
26
25
  const absolute = resolve(process.cwd(), path);
@@ -35,9 +34,9 @@ export const loadEnvFile = (path: string, required: boolean): boolean => {
35
34
  };
36
35
 
37
36
  /**
38
- * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads.
37
+ * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads,
38
+ * in name order and skipping any that are empty.
39
39
  *
40
- * @param env Environment to read.
41
40
  * @returns The tokens, in name order, skipping any that are empty.
42
41
  */
43
42
  export const tokensFromEnv = (
@@ -45,7 +44,7 @@ export const tokensFromEnv = (
45
44
  ): Array<PersonalAccessToken> =>
46
45
  Object.keys(env)
47
46
  .filter((name) => /^PAT_\d+$/.test(name))
48
- .sort()
47
+ .toSorted()
49
48
  .flatMap((name) => {
50
49
  const value = env[name];
51
50
  return value ? [{ name, value }] : [];
@@ -54,8 +53,6 @@ export const tokensFromEnv = (
54
53
  /**
55
54
  * The tokens a run will use.
56
55
  *
57
- * @param flags Tokens passed as `--pat`, which win over the environment.
58
- * @param env Environment to read, once any env file has been loaded into it.
59
56
  * @returns The tokens, empty when the run has none yet.
60
57
  */
61
58
  export const resolveTokens = (