@stats-forge/github-stats-forge-cli 0.0.2 → 0.2.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 +3 -8
- package/build/cards.d.ts.map +1 -1
- package/build/cards.js +74 -25
- 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 +16 -15
- 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 +10 -15
- package/build/saved-card.d.ts.map +1 -1
- package/build/saved-card.js +31 -23
- 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 +86 -32
- package/src/index.ts +30 -21
- package/src/prompts.ts +21 -19
- package/src/query.ts +4 -10
- package/src/saved-card.ts +40 -33
- package/src/spinner.ts +3 -6
- package/src/tokens.ts +6 -9
|
@@ -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;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,SAAU,QAAQ,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAuB7F,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,49 +19,58 @@ 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
|
-
* a boolean
|
|
52
|
+
* a boolean becomes one again, and a list splits back into its values,
|
|
53
|
+
* so each prompt opens on the answer it was saved with.
|
|
57
54
|
*
|
|
58
|
-
* @param card The card the params belong to.
|
|
59
|
-
* @param params The saved params.
|
|
60
55
|
* @returns The answers, ready for the menu.
|
|
61
56
|
*/
|
|
62
|
-
export const toAnswers = (card,
|
|
57
|
+
export const toAnswers = (card, options) => {
|
|
63
58
|
const kinds = new Map([...card.required, ...card.options].map((option) => [option.name, option.kind]));
|
|
64
|
-
|
|
65
|
-
name
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
const toAnswer = (name, value) => {
|
|
60
|
+
switch (kinds.get(name)) {
|
|
61
|
+
case 'boolean': {
|
|
62
|
+
return value === 'true';
|
|
63
|
+
}
|
|
64
|
+
case 'list': {
|
|
65
|
+
return value
|
|
66
|
+
.split(',')
|
|
67
|
+
.map((item) => item.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
default: {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return new Map(Object.entries(options).map(([name, value]) => [name, toAnswer(name, value)]));
|
|
68
76
|
};
|
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.2.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.2.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,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
contributedTo,
|
|
3
|
+
gist,
|
|
4
|
+
pin,
|
|
5
|
+
stats,
|
|
6
|
+
themes,
|
|
7
|
+
topLangs,
|
|
8
|
+
wakatime,
|
|
9
|
+
} from '@stats-forge/github-stats-forge-core/api';
|
|
10
|
+
import type { ApiResult, CardConfig } from '@stats-forge/github-stats-forge-core/api';
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
13
|
* @file What each card accepts, in the order the prompts walk it.
|
|
@@ -18,7 +26,7 @@ export interface CardOption {
|
|
|
18
26
|
/** What the prompt asks. */
|
|
19
27
|
label: string;
|
|
20
28
|
kind: OptionKind;
|
|
21
|
-
/** The accepted values, for `choice
|
|
29
|
+
/** The accepted values, for `choice` and for a `list` whose values are a closed set. */
|
|
22
30
|
choices?: ReadonlyArray<string>;
|
|
23
31
|
/** Shown under the prompt, for anything the label cannot say. */
|
|
24
32
|
hint?: string;
|
|
@@ -34,11 +42,7 @@ export interface CardKind {
|
|
|
34
42
|
required: ReadonlyArray<CardOption>;
|
|
35
43
|
/** Everything else, navigable in any order. */
|
|
36
44
|
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
|
-
*/
|
|
45
|
+
/** @returns The rendered card, or the rendered error. */
|
|
42
46
|
render: (query: Record<string, string>, config: CardConfig) => Promise<ApiResult>;
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -77,6 +81,19 @@ const COMMON_OPTIONS: ReadonlyArray<CardOption> = [
|
|
|
77
81
|
{ name: 'hide_border', label: 'Hide the border', kind: 'boolean' },
|
|
78
82
|
];
|
|
79
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The two ends of a card's date range.
|
|
86
|
+
*
|
|
87
|
+
* @returns The pair, naming what it counts.
|
|
88
|
+
*/
|
|
89
|
+
const rangeOptions = (counted: string): ReadonlyArray<CardOption> => {
|
|
90
|
+
const hint = 'A year, a month or a day: 2024, 2024-03, 2024-03-15';
|
|
91
|
+
return [
|
|
92
|
+
{ name: 'from', label: `Count ${counted} from`, kind: 'text', hint },
|
|
93
|
+
{ name: 'to', label: `Count ${counted} up to`, kind: 'text', hint },
|
|
94
|
+
];
|
|
95
|
+
};
|
|
96
|
+
|
|
80
97
|
const LOCALE_OPTION: CardOption = {
|
|
81
98
|
name: 'locale',
|
|
82
99
|
label: 'Locale',
|
|
@@ -95,13 +112,13 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
95
112
|
name: 'show',
|
|
96
113
|
label: 'Extra stats to show',
|
|
97
114
|
kind: 'list',
|
|
98
|
-
|
|
115
|
+
choices: stats.OPTIONS.show,
|
|
99
116
|
},
|
|
100
117
|
{
|
|
101
118
|
name: 'hide',
|
|
102
119
|
label: 'Stats to hide',
|
|
103
120
|
kind: 'list',
|
|
104
|
-
|
|
121
|
+
choices: stats.OPTIONS.hide,
|
|
105
122
|
},
|
|
106
123
|
{ name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
|
|
107
124
|
{ name: 'hide_rank', label: 'Hide the rank circle', kind: 'boolean' },
|
|
@@ -109,19 +126,14 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
109
126
|
name: 'rank_icon',
|
|
110
127
|
label: 'Rank indicator',
|
|
111
128
|
kind: 'choice',
|
|
112
|
-
choices: stats.
|
|
129
|
+
choices: stats.OPTIONS.rank_icon,
|
|
113
130
|
},
|
|
114
131
|
{
|
|
115
132
|
name: 'include_all_commits',
|
|
116
133
|
label: 'Count commits of all time',
|
|
117
134
|
kind: 'boolean',
|
|
118
135
|
},
|
|
119
|
-
|
|
120
|
-
name: 'commits_year',
|
|
121
|
-
label: 'Count commits for one year',
|
|
122
|
-
kind: 'number',
|
|
123
|
-
hint: 'Four digits, e.g. 2025',
|
|
124
|
-
},
|
|
136
|
+
...rangeOptions('commits'),
|
|
125
137
|
{
|
|
126
138
|
name: 'exclude_repo',
|
|
127
139
|
label: 'Repositories to exclude',
|
|
@@ -141,7 +153,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
141
153
|
name: 'role',
|
|
142
154
|
label: 'Owner affiliations to include',
|
|
143
155
|
kind: 'list',
|
|
144
|
-
|
|
156
|
+
choices: stats.OPTIONS.role,
|
|
145
157
|
},
|
|
146
158
|
{
|
|
147
159
|
name: 'contribs_include_own_repos',
|
|
@@ -157,7 +169,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
157
169
|
name: 'number_format',
|
|
158
170
|
label: 'Number format',
|
|
159
171
|
kind: 'choice',
|
|
160
|
-
choices:
|
|
172
|
+
choices: stats.OPTIONS.number_format,
|
|
161
173
|
},
|
|
162
174
|
{
|
|
163
175
|
name: 'number_precision',
|
|
@@ -172,7 +184,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
172
184
|
{ name: 'ring_color', label: 'Rank ring color', kind: 'text' },
|
|
173
185
|
LOCALE_OPTION,
|
|
174
186
|
],
|
|
175
|
-
render:
|
|
187
|
+
render: stats,
|
|
176
188
|
},
|
|
177
189
|
{
|
|
178
190
|
id: 'top-langs',
|
|
@@ -184,7 +196,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
184
196
|
name: 'layout',
|
|
185
197
|
label: 'Layout',
|
|
186
198
|
kind: 'choice',
|
|
187
|
-
choices: topLangs.
|
|
199
|
+
choices: topLangs.OPTIONS.layout,
|
|
188
200
|
},
|
|
189
201
|
{ name: 'langs_count', label: 'Languages to show', kind: 'number' },
|
|
190
202
|
{ name: 'hide', label: 'Languages to hide', kind: 'list' },
|
|
@@ -203,7 +215,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
203
215
|
name: 'stats_format',
|
|
204
216
|
label: 'Show values as',
|
|
205
217
|
kind: 'choice',
|
|
206
|
-
choices: topLangs.
|
|
218
|
+
choices: topLangs.OPTIONS.stats_format,
|
|
207
219
|
},
|
|
208
220
|
{
|
|
209
221
|
name: 'hide_progress',
|
|
@@ -216,7 +228,12 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
216
228
|
label: 'Progress bar background color',
|
|
217
229
|
kind: 'text',
|
|
218
230
|
},
|
|
219
|
-
{
|
|
231
|
+
{
|
|
232
|
+
name: 'role',
|
|
233
|
+
label: 'Owner affiliations to include',
|
|
234
|
+
kind: 'list',
|
|
235
|
+
choices: topLangs.OPTIONS.role,
|
|
236
|
+
},
|
|
220
237
|
{ name: 'custom_title', label: 'Card title', kind: 'text' },
|
|
221
238
|
{ name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
|
|
222
239
|
{ name: 'card_width', label: 'Card width', kind: 'number' },
|
|
@@ -227,7 +244,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
227
244
|
},
|
|
228
245
|
LOCALE_OPTION,
|
|
229
246
|
],
|
|
230
|
-
render:
|
|
247
|
+
render: topLangs,
|
|
231
248
|
},
|
|
232
249
|
{
|
|
233
250
|
id: 'pin',
|
|
@@ -243,7 +260,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
243
260
|
name: 'show',
|
|
244
261
|
label: 'Extra stats to show',
|
|
245
262
|
kind: 'list',
|
|
246
|
-
|
|
263
|
+
choices: pin.OPTIONS.show,
|
|
247
264
|
},
|
|
248
265
|
{ name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
|
|
249
266
|
{
|
|
@@ -263,11 +280,48 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
263
280
|
name: 'number_format',
|
|
264
281
|
label: 'Number format',
|
|
265
282
|
kind: 'choice',
|
|
266
|
-
choices:
|
|
283
|
+
choices: pin.OPTIONS.number_format,
|
|
284
|
+
},
|
|
285
|
+
LOCALE_OPTION,
|
|
286
|
+
],
|
|
287
|
+
render: pin,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'contributed-to',
|
|
291
|
+
label: 'Contributed to — repositories you work on, ranked',
|
|
292
|
+
needsToken: true,
|
|
293
|
+
required: [{ name: 'username', label: 'GitHub username', kind: 'text' }],
|
|
294
|
+
options: [
|
|
295
|
+
{ name: 'repos_count', label: 'Repositories to show', kind: 'number' },
|
|
296
|
+
{
|
|
297
|
+
name: 'include_own_repos',
|
|
298
|
+
label: 'Include your own repositories',
|
|
299
|
+
kind: 'boolean',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'exclude_repo',
|
|
303
|
+
label: 'Repositories to exclude',
|
|
304
|
+
kind: 'list',
|
|
305
|
+
hint: 'Each one an owner/name, or just the name',
|
|
306
|
+
},
|
|
307
|
+
...rangeOptions('contributions'),
|
|
308
|
+
{
|
|
309
|
+
name: 'hide_years',
|
|
310
|
+
label: 'Hide the year marks',
|
|
311
|
+
kind: 'boolean',
|
|
312
|
+
hint: 'One mark per contribution year, filled for the years that repo got one',
|
|
313
|
+
},
|
|
314
|
+
{ name: 'custom_title', label: 'Card title', kind: 'text' },
|
|
315
|
+
{ name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
|
|
316
|
+
{ name: 'card_width', label: 'Card width', kind: 'number' },
|
|
317
|
+
{
|
|
318
|
+
name: 'disable_animations',
|
|
319
|
+
label: 'Disable the animations',
|
|
320
|
+
kind: 'boolean',
|
|
267
321
|
},
|
|
268
322
|
LOCALE_OPTION,
|
|
269
323
|
],
|
|
270
|
-
render:
|
|
324
|
+
render: contributedTo,
|
|
271
325
|
},
|
|
272
326
|
{
|
|
273
327
|
id: 'gist',
|
|
@@ -281,8 +335,9 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
281
335
|
label: 'Let the browser wrap the description',
|
|
282
336
|
kind: 'boolean',
|
|
283
337
|
},
|
|
338
|
+
LOCALE_OPTION,
|
|
284
339
|
],
|
|
285
|
-
render:
|
|
340
|
+
render: gist,
|
|
286
341
|
},
|
|
287
342
|
{
|
|
288
343
|
id: 'wakatime',
|
|
@@ -294,13 +349,13 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
294
349
|
name: 'layout',
|
|
295
350
|
label: 'Layout',
|
|
296
351
|
kind: 'choice',
|
|
297
|
-
choices: wakatime.
|
|
352
|
+
choices: wakatime.OPTIONS.layout,
|
|
298
353
|
},
|
|
299
354
|
{
|
|
300
355
|
name: 'display_format',
|
|
301
356
|
label: 'Show values as',
|
|
302
357
|
kind: 'choice',
|
|
303
|
-
choices: wakatime.
|
|
358
|
+
choices: wakatime.OPTIONS.display_format,
|
|
304
359
|
},
|
|
305
360
|
{ name: 'langs_count', label: 'Languages to show', kind: 'number' },
|
|
306
361
|
{ name: 'hide', label: 'Languages to hide', kind: 'list' },
|
|
@@ -326,7 +381,7 @@ const CARDS: ReadonlyArray<CardKind> = [
|
|
|
326
381
|
},
|
|
327
382
|
LOCALE_OPTION,
|
|
328
383
|
],
|
|
329
|
-
render:
|
|
384
|
+
render: wakatime,
|
|
330
385
|
},
|
|
331
386
|
];
|
|
332
387
|
|
|
@@ -337,7 +392,6 @@ export const cards: ReadonlyArray<CardKind> = CARDS.map((card) => ({
|
|
|
337
392
|
}));
|
|
338
393
|
|
|
339
394
|
/**
|
|
340
|
-
* @param id The card's id, as `--card` takes it.
|
|
341
395
|
* @returns The card, or `undefined` when nothing renders under that name.
|
|
342
396
|
*/
|
|
343
397
|
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
|
}
|