@stats-forge/github-stats-forge-cli 0.0.1

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.
@@ -0,0 +1,35 @@
1
+ import type { PersonalAccessToken } from '@stats-forge/github-stats-forge-core';
2
+ /**
3
+ * @file Where the GitHub token comes from.
4
+ *
5
+ * Three sources, in the order a run should prefer them:
6
+ * the flag, an env file, then the environment the shell already carries.
7
+ * Whatever is left over is asked for interactively, so a first run needs no setup.
8
+ */
9
+ /** The file loaded when `--env-file` is not given. */
10
+ export declare const DEFAULT_ENV_FILE = ".env";
11
+ /**
12
+ * Loads an env file into `process.env`, the way `node --env-file` would.
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
+ * @throws {Error} When `required` and the file is not there.
18
+ */
19
+ export declare const loadEnvFile: (path: string, required: boolean) => boolean;
20
+ /**
21
+ * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads.
22
+ *
23
+ * @param env Environment to read.
24
+ * @returns The tokens, in name order, skipping any that are empty.
25
+ */
26
+ export declare const tokensFromEnv: (env: Record<string, string | undefined>) => Array<PersonalAccessToken>;
27
+ /**
28
+ * The tokens a run will use.
29
+ *
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
+ * @returns The tokens, empty when the run has none yet.
33
+ */
34
+ export declare const resolveTokens: (flags: ReadonlyArray<string>, env: Record<string, string | undefined>) => Array<PersonalAccessToken>;
35
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,57 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ /**
4
+ * @file Where the GitHub token comes from.
5
+ *
6
+ * Three sources, in the order a run should prefer them:
7
+ * the flag, an env file, then the environment the shell already carries.
8
+ * Whatever is left over is asked for interactively, so a first run needs no setup.
9
+ */
10
+ /** The file loaded when `--env-file` is not given. */
11
+ export const DEFAULT_ENV_FILE = '.env';
12
+ /**
13
+ * Loads an env file into `process.env`, the way `node --env-file` would.
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
+ * @throws {Error} When `required` and the file is not there.
19
+ */
20
+ export const loadEnvFile = (path, required) => {
21
+ const absolute = resolve(process.cwd(), path);
22
+ if (!existsSync(absolute)) {
23
+ if (required) {
24
+ throw new Error(`No env file at ${absolute}`);
25
+ }
26
+ return false;
27
+ }
28
+ process.loadEnvFile(absolute);
29
+ return true;
30
+ };
31
+ /**
32
+ * The tokens an env holds, under the `PAT_1`, `PAT_2`, … names core reads.
33
+ *
34
+ * @param env Environment to read.
35
+ * @returns The tokens, in name order, skipping any that are empty.
36
+ */
37
+ export const tokensFromEnv = (env) => Object.keys(env)
38
+ .filter((name) => /^PAT_\d+$/.test(name))
39
+ .sort()
40
+ .flatMap((name) => {
41
+ const value = env[name];
42
+ return value ? [{ name, value }] : [];
43
+ });
44
+ /**
45
+ * The tokens a run will use.
46
+ *
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
+ * @returns The tokens, empty when the run has none yet.
50
+ */
51
+ export const resolveTokens = (flags, env) => {
52
+ const fromFlags = flags
53
+ .map((value) => value.trim())
54
+ .filter(Boolean)
55
+ .map((value, index) => ({ name: `--pat #${index + 1}`, value }));
56
+ return fromFlags.length > 0 ? fromFlags : tokensFromEnv(env);
57
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@stats-forge/github-stats-forge-cli",
3
+ "version": "0.0.1",
4
+ "description": "Render a GitHub stats card to a local SVG file, one prompt at a time",
5
+ "keywords": [
6
+ "cli",
7
+ "github-stats",
8
+ "github-stats-forge",
9
+ "stats-forge"
10
+ ],
11
+ "homepage": "https://github.com/stats-forge/github-stats-forge",
12
+ "bugs": {
13
+ "url": "https://github.com/stats-forge/github-stats-forge/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": {
17
+ "name": "Marco Pasqualetti",
18
+ "url": "https://github.com/marcalexiei"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/stats-forge/github-stats-forge.git",
23
+ "directory": "packages/cli"
24
+ },
25
+ "bin": {
26
+ "github-stats-forge": "./build/index.js"
27
+ },
28
+ "files": [
29
+ "build",
30
+ "src"
31
+ ],
32
+ "type": "module",
33
+ "exports": {
34
+ ".": {
35
+ "@stats/source": "./src/index.ts",
36
+ "default": "./build/index.js"
37
+ }
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@inquirer/prompts": "8.6.0",
44
+ "@stats-forge/github-stats-forge-core": "^0.0.1"
45
+ },
46
+ "devDependencies": {
47
+ "vitest": "4.1.10"
48
+ },
49
+ "engines": {
50
+ "node": ">=24"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.build.json",
54
+ "dev": "pnpm run build && node build/index.js",
55
+ "test": "vitest",
56
+ "lint": "eslint",
57
+ "typecheck": "tsc -p tsconfig.typecheck.json"
58
+ }
59
+ }
package/src/cards.ts ADDED
@@ -0,0 +1,343 @@
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';
3
+
4
+ /**
5
+ * @file What each card accepts, in the order the prompts walk it.
6
+ *
7
+ * The core schemas validate these params;
8
+ * this catalog is what makes them navigable, so it carries the prose and the choices a schema has no room for.
9
+ */
10
+
11
+ /** How a param is asked for, and how the answer becomes a query string value. */
12
+ type OptionKind = 'text' | 'boolean' | 'number' | 'list' | 'choice';
13
+
14
+ /** One param of one card. */
15
+ export interface CardOption {
16
+ /** Query param the answer is written to. */
17
+ name: string;
18
+ /** What the prompt asks. */
19
+ label: string;
20
+ kind: OptionKind;
21
+ /** The accepted values, for `choice`. */
22
+ choices?: ReadonlyArray<string>;
23
+ /** Shown under the prompt, for anything the label cannot say. */
24
+ hint?: string;
25
+ }
26
+
27
+ /** A card, its params, and the core handler that renders it. */
28
+ export interface CardKind {
29
+ id: string;
30
+ label: string;
31
+ /** Whether rendering it calls the GitHub API, and so needs a token. */
32
+ needsToken: boolean;
33
+ /** Asked first: the card renders nothing without them. */
34
+ required: ReadonlyArray<CardOption>;
35
+ /** Everything else, navigable in any order. */
36
+ 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
+ */
42
+ render: (query: Record<string, string>, config: CardConfig) => Promise<ApiResult>;
43
+ }
44
+
45
+ const THEME_NAMES = Object.keys(themes);
46
+
47
+ /** Colors and the theme, which every card accepts. */
48
+ const COMMON_OPTIONS: ReadonlyArray<CardOption> = [
49
+ {
50
+ name: 'theme',
51
+ label: 'Theme',
52
+ kind: 'choice',
53
+ choices: THEME_NAMES,
54
+ hint: 'An unknown name falls back to the default theme',
55
+ },
56
+ {
57
+ name: 'title_color',
58
+ label: 'Title color',
59
+ kind: 'text',
60
+ hint: 'Hex, no #',
61
+ },
62
+ { name: 'text_color', label: 'Text color', kind: 'text', hint: 'Hex, no #' },
63
+ { name: 'icon_color', label: 'Icon color', kind: 'text', hint: 'Hex, no #' },
64
+ {
65
+ name: 'bg_color',
66
+ label: 'Background color',
67
+ kind: 'text',
68
+ hint: 'Hex, no #, or a gradient: angle,color,color',
69
+ },
70
+ {
71
+ name: 'border_color',
72
+ label: 'Border color',
73
+ kind: 'text',
74
+ hint: 'Hex, no #',
75
+ },
76
+ { name: 'border_radius', label: 'Border radius', kind: 'number' },
77
+ { name: 'hide_border', label: 'Hide the border', kind: 'boolean' },
78
+ ];
79
+
80
+ const LOCALE_OPTION: CardOption = {
81
+ name: 'locale',
82
+ label: 'Locale',
83
+ kind: 'text',
84
+ hint: 'Two-letter code, e.g. es',
85
+ };
86
+
87
+ const CARDS: ReadonlyArray<CardKind> = [
88
+ {
89
+ id: 'stats',
90
+ label: 'Stats — commits, PRs, issues, reviews and a rank',
91
+ needsToken: true,
92
+ required: [{ name: 'username', label: 'GitHub username', kind: 'text' }],
93
+ options: [
94
+ {
95
+ name: 'show',
96
+ label: 'Extra stats to show',
97
+ kind: 'list',
98
+ hint: 'e.g. reviews,discussions_started,prs_merged,contributions',
99
+ },
100
+ {
101
+ name: 'hide',
102
+ label: 'Stats to hide',
103
+ kind: 'list',
104
+ hint: 'e.g. stars,commits,prs,issues,contribs',
105
+ },
106
+ { name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
107
+ { name: 'hide_rank', label: 'Hide the rank circle', kind: 'boolean' },
108
+ {
109
+ name: 'rank_icon',
110
+ label: 'Rank indicator',
111
+ kind: 'choice',
112
+ choices: stats.RANK_ICONS,
113
+ },
114
+ {
115
+ name: 'include_all_commits',
116
+ label: 'Count commits of all time',
117
+ kind: 'boolean',
118
+ },
119
+ {
120
+ name: 'commits_year',
121
+ label: 'Count commits for one year',
122
+ kind: 'number',
123
+ hint: 'Four digits, e.g. 2025',
124
+ },
125
+ {
126
+ name: 'exclude_repo',
127
+ label: 'Repositories to exclude',
128
+ kind: 'list',
129
+ },
130
+ {
131
+ name: 'repo',
132
+ label: 'Repositories the search-based stats are scoped to',
133
+ kind: 'list',
134
+ },
135
+ {
136
+ name: 'owner',
137
+ label: 'Owners the search-based stats are scoped to',
138
+ kind: 'list',
139
+ },
140
+ {
141
+ name: 'role',
142
+ label: 'Owner affiliations to include',
143
+ kind: 'list',
144
+ hint: 'OWNER, COLLABORATOR, ORGANIZATION_MEMBER',
145
+ },
146
+ {
147
+ name: 'contribs_include_own_repos',
148
+ label: 'Count contributions to your own repositories',
149
+ kind: 'boolean',
150
+ },
151
+ { name: 'custom_title', label: 'Card title', kind: 'text' },
152
+ { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
153
+ { name: 'card_width', label: 'Card width', kind: 'number' },
154
+ { name: 'line_height', label: 'Line height', kind: 'number' },
155
+ { name: 'text_bold', label: 'Bold stat values', kind: 'boolean' },
156
+ {
157
+ name: 'number_format',
158
+ label: 'Number format',
159
+ kind: 'choice',
160
+ choices: ['short', 'long'],
161
+ },
162
+ {
163
+ name: 'number_precision',
164
+ label: 'Decimals kept when abbreviating',
165
+ kind: 'number',
166
+ },
167
+ {
168
+ name: 'disable_animations',
169
+ label: 'Disable the animations',
170
+ kind: 'boolean',
171
+ },
172
+ { name: 'ring_color', label: 'Rank ring color', kind: 'text' },
173
+ LOCALE_OPTION,
174
+ ],
175
+ render: (query, config) => stats(query, config),
176
+ },
177
+ {
178
+ id: 'top-langs',
179
+ label: 'Top languages — the languages you write most',
180
+ needsToken: true,
181
+ required: [{ name: 'username', label: 'GitHub username', kind: 'text' }],
182
+ options: [
183
+ {
184
+ name: 'layout',
185
+ label: 'Layout',
186
+ kind: 'choice',
187
+ choices: topLangs.LAYOUTS,
188
+ },
189
+ { name: 'langs_count', label: 'Languages to show', kind: 'number' },
190
+ { name: 'hide', label: 'Languages to hide', kind: 'list' },
191
+ { name: 'exclude_repo', label: 'Repositories to exclude', kind: 'list' },
192
+ {
193
+ name: 'size_weight',
194
+ label: "Weight given to a language's size",
195
+ kind: 'number',
196
+ },
197
+ {
198
+ name: 'count_weight',
199
+ label: 'Weight given to its repository count',
200
+ kind: 'number',
201
+ },
202
+ {
203
+ name: 'stats_format',
204
+ label: 'Show values as',
205
+ kind: 'choice',
206
+ choices: topLangs.STATS_FORMATS,
207
+ },
208
+ {
209
+ name: 'hide_progress',
210
+ label: 'Hide the progress bars',
211
+ kind: 'boolean',
212
+ },
213
+ { name: 'hide_values', label: 'Hide the values', kind: 'boolean' },
214
+ {
215
+ name: 'prog_bar_bg_color',
216
+ label: 'Progress bar background color',
217
+ kind: 'text',
218
+ },
219
+ { name: 'role', label: 'Owner affiliations to include', kind: 'list' },
220
+ { name: 'custom_title', label: 'Card title', kind: 'text' },
221
+ { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
222
+ { name: 'card_width', label: 'Card width', kind: 'number' },
223
+ {
224
+ name: 'disable_animations',
225
+ label: 'Disable the animations',
226
+ kind: 'boolean',
227
+ },
228
+ LOCALE_OPTION,
229
+ ],
230
+ render: (query, config) => topLangs(query, config),
231
+ },
232
+ {
233
+ id: 'pin',
234
+ label: 'Repository pin — one repository',
235
+ needsToken: true,
236
+ required: [
237
+ { name: 'username', label: 'GitHub username', kind: 'text' },
238
+ { name: 'repo', label: 'Repository name', kind: 'text' },
239
+ ],
240
+ options: [
241
+ { name: 'show_owner', label: 'Show the owner', kind: 'boolean' },
242
+ {
243
+ name: 'show',
244
+ label: 'Extra stats to show',
245
+ kind: 'list',
246
+ hint: 'e.g. prs_authored,issues_commented',
247
+ },
248
+ { name: 'show_icons', label: 'Show the stat icons', kind: 'boolean' },
249
+ {
250
+ name: 'description_lines_count',
251
+ label: 'Lines the description wraps to',
252
+ kind: 'number',
253
+ },
254
+ {
255
+ name: 'browser_rendering',
256
+ label: 'Let the browser wrap the description',
257
+ kind: 'boolean',
258
+ },
259
+ { name: 'card_width', label: 'Card width', kind: 'number' },
260
+ { name: 'line_height', label: 'Line height', kind: 'number' },
261
+ { name: 'text_bold', label: 'Bold stat values', kind: 'boolean' },
262
+ {
263
+ name: 'number_format',
264
+ label: 'Number format',
265
+ kind: 'choice',
266
+ choices: ['short', 'long'],
267
+ },
268
+ LOCALE_OPTION,
269
+ ],
270
+ render: (query, config) => pin(query, config),
271
+ },
272
+ {
273
+ id: 'gist',
274
+ label: 'Gist pin — one gist',
275
+ needsToken: true,
276
+ required: [{ name: 'id', label: 'Gist ID', kind: 'text' }],
277
+ options: [
278
+ { name: 'show_owner', label: 'Show the owner', kind: 'boolean' },
279
+ {
280
+ name: 'browser_rendering',
281
+ label: 'Let the browser wrap the description',
282
+ kind: 'boolean',
283
+ },
284
+ ],
285
+ render: (query, config) => gist(query, config),
286
+ },
287
+ {
288
+ id: 'wakatime',
289
+ label: 'WakaTime — coding time per language',
290
+ needsToken: false,
291
+ required: [{ name: 'username', label: 'WakaTime username', kind: 'text' }],
292
+ options: [
293
+ {
294
+ name: 'layout',
295
+ label: 'Layout',
296
+ kind: 'choice',
297
+ choices: wakatime.LAYOUTS,
298
+ },
299
+ {
300
+ name: 'display_format',
301
+ label: 'Show values as',
302
+ kind: 'choice',
303
+ choices: wakatime.DISPLAY_FORMATS,
304
+ },
305
+ { name: 'langs_count', label: 'Languages to show', kind: 'number' },
306
+ { name: 'hide', label: 'Languages to hide', kind: 'list' },
307
+ {
308
+ name: 'hide_progress',
309
+ label: 'Hide the progress bars',
310
+ kind: 'boolean',
311
+ },
312
+ { name: 'custom_title', label: 'Card title', kind: 'text' },
313
+ { name: 'hide_title', label: 'Hide the title', kind: 'boolean' },
314
+ { name: 'card_width', label: 'Card width', kind: 'number' },
315
+ { name: 'line_height', label: 'Line height', kind: 'number' },
316
+ {
317
+ name: 'disable_animations',
318
+ label: 'Disable the animations',
319
+ kind: 'boolean',
320
+ },
321
+ {
322
+ name: 'api_domain',
323
+ label: 'WakaTime instance',
324
+ kind: 'text',
325
+ hint: 'Defaults to wakatime.com',
326
+ },
327
+ LOCALE_OPTION,
328
+ ],
329
+ render: (query, config) => wakatime(query, config),
330
+ },
331
+ ];
332
+
333
+ /** Every card, with the options every card shares appended to its own. */
334
+ export const cards: ReadonlyArray<CardKind> = CARDS.map((card) => ({
335
+ ...card,
336
+ options: [...card.options, ...COMMON_OPTIONS],
337
+ }));
338
+
339
+ /**
340
+ * @param id The card's id, as `--card` takes it.
341
+ * @returns The card, or `undefined` when nothing renders under that name.
342
+ */
343
+ export const findCard = (id: string): CardKind | undefined => cards.find((card) => card.id === id);