@stats-forge/github-stats-forge-cli 0.0.1 → 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 +4 -4
- package/build/cards.d.ts +2 -7
- package/build/cards.d.ts.map +1 -1
- package/build/cards.js +1 -2
- package/build/index.js +9 -12
- package/build/prompts.d.ts +5 -12
- package/build/prompts.d.ts.map +1 -1
- package/build/prompts.js +6 -14
- package/build/query.d.ts +2 -8
- package/build/query.d.ts.map +1 -1
- package/build/query.js +3 -9
- package/build/saved-card.d.ts +8 -14
- package/build/saved-card.d.ts.map +1 -1
- package/build/saved-card.js +13 -19
- package/build/spinner.d.ts +0 -3
- package/build/spinner.d.ts.map +1 -1
- package/build/spinner.js +3 -6
- package/build/tokens.d.ts +5 -8
- package/build/tokens.d.ts.map +1 -1
- package/build/tokens.js +5 -8
- package/package.json +8 -8
- package/src/cards.ts +10 -8
- package/src/index.ts +30 -21
- package/src/prompts.ts +10 -18
- package/src/query.ts +4 -10
- package/src/saved-card.ts +20 -26
- package/src/spinner.ts +3 -6
- package/src/tokens.ts +6 -9
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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;
|
package/build/cards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../src/cards.ts"],"names":[],"mappings":"
|
|
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')
|
|
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.
|
|
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.
|
|
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 (
|
|
180
|
+
catch (error) {
|
|
184
181
|
// A cancelled prompt is a normal way to leave, not a crash.
|
|
185
|
-
if (
|
|
182
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
186
183
|
process.exitCode = 130;
|
|
187
184
|
}
|
|
188
185
|
else {
|
|
189
|
-
process.stderr.write(`${
|
|
186
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
190
187
|
process.exitCode = 1;
|
|
191
188
|
}
|
|
192
189
|
}
|
package/build/prompts.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import type { CardKind, CardOption } from './cards.
|
|
2
|
-
import type { Answer } from './query.
|
|
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
|
|
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
|
|
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>;
|
package/build/prompts.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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;
|
package/build/query.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
46
|
+
const { repo } = query;
|
|
53
47
|
const parts = [card.id, subject, repo].filter(Boolean).join('-');
|
|
54
|
-
return `${parts.
|
|
48
|
+
return `${parts.replaceAll(/[^\w.-]/g, '-')}.svg`;
|
|
55
49
|
};
|
package/build/saved-card.d.ts
CHANGED
|
@@ -1,39 +1,33 @@
|
|
|
1
|
-
import type { CardKind } from './cards.
|
|
2
|
-
import type { Answer } from './query.
|
|
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
|
-
|
|
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,
|
|
23
|
+
export declare const writeSavedCard: (path: string, card: CardKind, options: Record<string, string>) => Promise<string>;
|
|
28
24
|
/**
|
|
29
|
-
* Turns saved
|
|
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,
|
|
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
|
|
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"}
|
package/build/saved-card.js
CHANGED
|
@@ -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 (
|
|
24
|
-
throw new Error(`${file} is not readable as JSON`, { cause:
|
|
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,
|
|
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
|
-
//
|
|
35
|
-
const entries = Object.entries(
|
|
36
|
-
return { card,
|
|
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,
|
|
42
|
+
export const writeSavedCard = async (path, card, options) => {
|
|
47
43
|
const file = resolve(process.cwd(), path);
|
|
48
|
-
const saved = { card: card.id,
|
|
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
|
|
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,
|
|
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(
|
|
58
|
+
return new Map(Object.entries(options).map(([name, value]) => [
|
|
65
59
|
name,
|
|
66
60
|
kinds.get(name) === 'boolean' ? value === 'true' : value,
|
|
67
61
|
]));
|
package/build/spinner.d.ts
CHANGED
|
@@ -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>;
|
package/build/spinner.d.ts.map
CHANGED
|
@@ -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
|
|
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 = '\
|
|
11
|
-
const SHOW_CURSOR = '\
|
|
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\
|
|
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>;
|
package/build/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,
|
|
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
|
-
.
|
|
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
|
|
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.
|
|
44
|
-
"@stats-forge/github-stats-forge-core": "^0.0
|
|
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.
|
|
47
|
+
"vitest": "4.1.11"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
|
-
"node": ">=
|
|
50
|
+
"node": "^24 || >=26"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsc -p tsconfig.build.json",
|
|
54
|
-
"
|
|
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 {
|
|
2
|
-
|
|
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.
|
|
9
|
-
import { cards, findCard } from './cards.
|
|
10
|
-
import type { Menu } from './prompts.
|
|
11
|
-
import { askRequired, askSavePath, askToken, navigateOptions, pickCard } from './prompts.
|
|
12
|
-
import { defaultFileName, toQuery } from './query.
|
|
13
|
-
import { readSavedCard, savedCardExists, toAnswers, writeSavedCard } from './saved-card.
|
|
14
|
-
import { withSpinner } from './spinner.
|
|
15
|
-
import { DEFAULT_ENV_FILE, loadEnvFile, resolveTokens } from './tokens.
|
|
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')
|
|
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.
|
|
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.
|
|
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 (
|
|
244
|
+
} catch (error) {
|
|
236
245
|
// A cancelled prompt is a normal way to leave, not a crash.
|
|
237
|
-
if (
|
|
246
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
238
247
|
process.exitCode = 130;
|
|
239
248
|
} else {
|
|
240
|
-
process.stderr.write(`${
|
|
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.
|
|
4
|
-
import { cards } from './cards.
|
|
5
|
-
import type { Answer } from './query.
|
|
6
|
-
import { describeAnswer } from './query.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
61
|
+
const { repo } = query;
|
|
68
62
|
const parts = [card.id, subject, repo].filter(Boolean).join('-');
|
|
69
|
-
return `${parts.
|
|
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.
|
|
6
|
-
import { findCard } from './cards.
|
|
7
|
-
import type { Answer } from './query.
|
|
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
|
|
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
|
|
21
|
-
|
|
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;
|
|
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 (
|
|
45
|
-
throw new Error(`${file} is not readable as JSON`, { cause:
|
|
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,
|
|
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
|
-
//
|
|
59
|
-
const entries = Object.entries(
|
|
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,
|
|
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
|
-
|
|
73
|
+
options: Record<string, string>,
|
|
78
74
|
): Promise<string> => {
|
|
79
75
|
const file = resolve(process.cwd(), path);
|
|
80
|
-
const saved: SavedCard = { card: card.id,
|
|
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
|
|
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,
|
|
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(
|
|
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 = '\
|
|
13
|
-
const SHOW_CURSOR = '\
|
|
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\
|
|
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
|
-
.
|
|
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 = (
|