• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gittrends-app / github-proxy-server / 21291878515

23 Jan 2026 03:42PM UTC coverage: 79.678% (-0.09%) from 79.771%
21291878515

push

github

hsborges
v12.2.0

144 of 196 branches covered (73.47%)

Branch coverage included in aggregate %.

252 of 301 relevant lines covered (83.72%)

107.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.58
/src/cli.ts
1
#!/usr/bin/env node
2
/* Author: Hudson S. Borges */
3
import EventEmitter from 'node:events';
4
import { pathToFileURL } from 'node:url';
5

6
import chalk from 'chalk';
7
import { Command, Option } from 'commander';
8
import consola from 'consola';
9
import ip from 'ip';
10
import isNil from 'lodash/isNil.js';
11
import isObjectLike from 'lodash/isObjectLike.js';
12
import omit from 'lodash/omit.js';
13
import omitBy from 'lodash/omitBy.js';
14

15
import packageJson from '../package.json' with { type: 'json' };
16
import { type CliOpts, concatTokens, createProxyServer, readTokensFile } from './server.js';
17

18
function parseTimeBudgetMultiplier(value: string): number {
19
  const num = Number(value);
×
20
  if (isNaN(num) || num < 1) {
×
21
    throw new Error('Time budget multiplier must be >= 1.0 (use 1.0 for 100%, 1.5 for 150%, etc.)');
×
22
  }
23
  return num;
×
24
}
25

26
export function createCli(): Command {
27
  const program = new Command();
27✔
28

29
  return program
27✔
30
    .addOption(
31
      new Option('-p, --port [port]', 'Port to start the proxy server')
32
        .argParser(Number)
33
        .default(3000)
34
        .env('PORT')
35
    )
36
    .addOption(
37
      new Option('-t, --token [token]', 'GitHub token to be used')
38
        .argParser(concatTokens)
39
        .default([])
40
    )
41
    .addOption(
42
      new Option('--tokens [file]', 'File containing a list of tokens')
43
        .argParser(readTokensFile)
44
        .env('GPS_TOKENS_FILE')
45
    )
46
    .addOption(
47
      new Option('--request-timeout [timeout]', 'Request timeout (ms)')
48
        .argParser(Number)
49
        .default(30000)
50
        .env('GPS_REQUEST_TIMEOUT')
51
    )
52
    .addOption(
53
      new Option('--min-remaining <number>', 'Stop using token on a minimum of')
54
        .argParser(Number)
55
        .default(100)
56
        .env('GPS_MIN_REMAINING')
57
    )
58
    .addOption(
59
      new Option('--time-budget-multiplier [multiplier]', 'Time budget multiplier (>= 1.0)')
60
        .argParser(parseTimeBudgetMultiplier)
61
        .default(1)
62
        .env('GPS_TIME_BUDGET_MULTIPLIER')
63
    )
64
    .addOption(new Option('--silent', 'Dont show requests outputs').env('GPS_SILENT'))
65
    .addOption(
66
      new Option(
67
        '--no-override-authorization',
68
        'By default, the authorization header is overrided with a configured token'
69
      )
70
    )
71
    .addOption(
72
      new Option('--auth-username [username]', 'Proxy authentication username').env(
73
        'GPS_AUTH_USERNAME'
74
      )
75
    )
76
    .addOption(
77
      new Option('--auth-password [password]', 'Proxy authentication password').env(
78
        'GPS_AUTH_PASSWORD'
79
      )
80
    )
81
    .addOption(new Option('--no-status-monitor', 'Disable requests monitoring on /status'))
82
    .version(packageJson.version || '?', '-v, --version', 'output the current version')
27!
83
    .action(async (options) => {
84
      if (!options.token.length && !options.tokens?.length) {
×
85
        consola.info(`${program.helpInformation()}`);
×
86
        consola.error(`Arguments missing ("--token" or "--tokens" is mandatory).\n\n`);
×
87
        process.exit(1);
×
88
      }
89

90
      EventEmitter.defaultMaxListeners = Number.MAX_SAFE_INTEGER;
×
91

92
      const tokens = [...options.token, ...(options.tokens || [])].reduce(
×
93
        (memo: string[], token: string) => concatTokens(token, memo),
×
94
        []
95
      );
96

97
      const appOptions: CliOpts = {
×
98
        requestTimeout: options.requestTimeout,
99
        silent: options.silent,
100
        overrideAuthorization: options.overrideAuthorization,
101
        tokens: tokens,
102
        minRemaining: options.minRemaining,
103
        timeBudgetMultiplier: options.timeBudgetMultiplier,
104
        statusMonitor: options.statusMonitor,
105
        auth:
106
          options.authUsername && options.authPassword
×
107
            ? {
108
                username: options.authUsername,
109
                password: options.authPassword
110
              }
111
            : undefined
112
      };
113

114
      const app = createProxyServer(appOptions);
×
115

116
      app
×
117
        .on('log', (data) => process.stdout.write(data.toString()))
×
118
        .on('warn', consola.warn)
119
        .on('error', consola.error);
120

121
      const server = app.listen({ host: '0.0.0.0', port: options.port }, (error?: Error) => {
×
122
        if (error) {
×
123
          consola.error(error);
×
124
          process.exit(1);
×
125
        }
126

127
        const host = `http://${ip.address()}:${options.port}`;
×
128
        consola.success(
×
129
          `Proxy server running on ${host} (tokens: ${chalk.greenBright(tokens.length)})`
130
        );
131

132
        function formatObject(object: Record<string, unknown>): string {
133
          return Object.entries(omitBy(object, (value) => isNil(value)))
×
134
            .sort((a: [string, unknown], b: [string, unknown]) => (a[0] > b[0] ? 1 : -1))
×
135
            .map(
136
              ([k, v]) =>
137
                `${k}: ${
×
138
                  isObjectLike(v)
139
                    ? `{ ${formatObject(v as Record<string, unknown>)} }`
140
                    : chalk.greenBright(v)
141
                }`
142
            )
143
            .join(', ');
144
        }
145

146
        consola.success(
×
147
          `${chalk.bold('Options')}: %s`,
148
          formatObject(omit(appOptions, ['token', 'tokens']))
149
        );
150
      });
151

152
      const shutdown = async () => {
×
153
        server.close((err?: Error) => {
×
154
          if (err) {
×
155
            consola.error(err);
×
156
            process.exit(1);
×
157
          }
158

159
          consola.success('Server closed');
×
160
          process.exit(0);
×
161
        });
162
      };
163

164
      ['SIGTERM', 'SIGINT'].forEach((signal) => {
×
165
        process.on(signal, async () => {
×
166
          consola.info(`${signal} signal received: closing HTTP server`);
×
167
          await shutdown();
×
168
        });
169
      });
170
    });
171
}
172

173
// parse arguments from command line
174
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
1!
175
  createCli().parse(process.argv);
×
176
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc