@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.
@@ -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;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,SAAU,QAAQ,WAAW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAuB7F,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,49 +19,58 @@ 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
- * a boolean option becomes one again so its prompt opens on the right answer.
52
+ * a boolean becomes one again, and a list splits back into its values,
53
+ * so each prompt opens on the answer it was saved with.
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, params) => {
57
+ export const toAnswers = (card, options) => {
63
58
  const kinds = new Map([...card.required, ...card.options].map((option) => [option.name, option.kind]));
64
- return new Map(Object.entries(params).map(([name, value]) => [
65
- name,
66
- kinds.get(name) === 'boolean' ? value === 'true' : value,
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
  };
@@ -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.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.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.2.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,13 @@
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
+ 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
- hint: 'e.g. reviews,discussions_started,prs_merged,contributions',
115
+ choices: stats.OPTIONS.show,
99
116
  },
100
117
  {
101
118
  name: 'hide',
102
119
  label: 'Stats to hide',
103
120
  kind: 'list',
104
- hint: 'e.g. stars,commits,prs,issues,contribs',
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.RANK_ICONS,
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
- hint: 'OWNER, COLLABORATOR, ORGANIZATION_MEMBER',
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: ['short', 'long'],
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: (query, config) => stats(query, config),
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.LAYOUTS,
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.STATS_FORMATS,
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
- { name: 'role', label: 'Owner affiliations to include', kind: 'list' },
231
+ {
232
+ name: 'role',
233
+ label: 'Owner affiliations to include',
234
+ kind: 'list',
235
+ choices: topLangs.OPTIONS.role,
236
+ },
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: (query, config) => topLangs(query, config),
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
- hint: 'e.g. prs_authored,issues_commented',
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: ['short', 'long'],
283
+ choices: pin.OPTIONS.number_format,
284
+ },
285
+ LOCALE_OPTION,
286
+ ],
287
+ render: pin,
288
+ },
289
+ {
290
+ id: 'contributed-to',
291
+ label: 'Contributed to — repositories you work on, ranked',
292
+ needsToken: true,
293
+ required: [{ name: 'username', label: 'GitHub username', kind: 'text' }],
294
+ options: [
295
+ { name: 'repos_count', label: 'Repositories to show', kind: 'number' },
296
+ {
297
+ name: 'include_own_repos',
298
+ label: 'Include your own repositories',
299
+ kind: 'boolean',
300
+ },
301
+ {
302
+ name: 'exclude_repo',
303
+ label: 'Repositories to exclude',
304
+ kind: 'list',
305
+ hint: 'Each one an owner/name, or just the name',
306
+ },
307
+ ...rangeOptions('contributions'),
308
+ {
309
+ name: 'hide_years',
310
+ label: 'Hide the year marks',
311
+ kind: 'boolean',
312
+ hint: 'One mark per contribution year, filled for the years that repo got one',
313
+ },
314
+ { name: 'custom_title', label: 'Card title', kind: 'text' },
315
+ { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
316
+ { name: 'card_width', label: 'Card width', kind: 'number' },
317
+ {
318
+ name: 'disable_animations',
319
+ label: 'Disable the animations',
320
+ kind: 'boolean',
267
321
  },
268
322
  LOCALE_OPTION,
269
323
  ],
270
- render: (query, config) => pin(query, config),
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: (query, config) => gist(query, config),
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.LAYOUTS,
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.DISPLAY_FORMATS,
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: (query, config) => wakatime(query, config),
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.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
  }