@gitping/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.
@@ -1,98 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { listPings } from '../api/pings.ts'
4
- import { getToken } from '../auth/keychain.ts'
5
- import type { PingStatus } from '@gitping/shared'
6
- import type { PingWithThread } from '../api/pings.ts'
7
-
8
- interface InboxOptions {
9
- unread?: boolean
10
- watch?: boolean
11
- json?: boolean
12
- }
13
-
14
- export async function inboxCommand(options: InboxOptions): Promise<void> {
15
- const token = await getToken()
16
- if (!token) {
17
- console.error(chalk.red('Not logged in. Run `gitping login` first.'))
18
- throw Object.assign(new Error('Not authenticated'), { exitCode: 2 })
19
- }
20
-
21
- if (options.watch) {
22
- // Lazy-load ink to prevent startup crash on React/reconciler version conflicts
23
- const { render } = await import('ink')
24
- const React = await import('react')
25
- const { default: InboxWatch } = await import('../ui/InboxWatch.tsx')
26
- render(React.createElement(InboxWatch, { token }))
27
- return
28
- }
29
-
30
- const filter = options.unread ? 'unread' : undefined
31
- const spinner = ora('Loading inbox…').start()
32
-
33
- let page
34
- try {
35
- page = await listPings({ filter })
36
- } catch (err) {
37
- spinner.fail('Failed to load inbox')
38
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
39
- }
40
-
41
- spinner.stop()
42
-
43
- if (options.json) {
44
- console.log(JSON.stringify(page))
45
- return
46
- }
47
-
48
- if (page.data.length === 0) {
49
- console.log(chalk.dim(' No pings yet. Your inbox is empty.'))
50
- return
51
- }
52
-
53
- console.log()
54
- console.log(chalk.cyan.bold(' ⚡ GitPing Inbox') + chalk.dim(` (${page.data.length} pings)`))
55
- console.log(chalk.dim(' ─────────────────────────────────────────'))
56
- console.log()
57
- for (const ping of page.data) {
58
- printPing(ping)
59
- }
60
-
61
- if (page.nextCursor) {
62
- console.log(chalk.dim(`\n More results available. Use --cursor ${page.nextCursor}`))
63
- }
64
- }
65
-
66
- export function printPing(ping: PingWithThread): void {
67
- const statusColor = STATUS_COLORS[ping.status] ?? chalk.white
68
- const ts = new Date(ping.created_at).toLocaleString()
69
- const idShort = ping.id.slice(0, 8)
70
-
71
- // Status badge with visual indicator
72
- const statusBadge = statusColor(`[${ping.status}]`)
73
- const categoryBadge = chalk.yellow(`[${ping.category}]`)
74
-
75
- console.log(
76
- ` ${chalk.dim(idShort)} ` +
77
- `${chalk.cyan.bold('@' + ping.sender_username)} ${chalk.dim('→')} ${chalk.cyan.bold('@' + ping.recipient_username)} ` +
78
- `${statusBadge} ${categoryBadge}`,
79
- )
80
- console.log(` ${chalk.dim('│')} ${ping.message}`)
81
- console.log(` ${chalk.dim('│')} ${chalk.dim(ts)}`)
82
- if (ping.status === 'delivered') {
83
- console.log(
84
- ` ${chalk.dim('└')} ${chalk.green('gitping accept @' + ping.sender_username)} · ` +
85
- `${chalk.dim('gitping ignore @' + ping.sender_username)} · ` +
86
- `${chalk.red('gitping block @' + ping.sender_username)}`,
87
- )
88
- }
89
- console.log()
90
- }
91
-
92
- const STATUS_COLORS: Record<PingStatus, (s: string) => string> = {
93
- pending: chalk.gray,
94
- delivered: chalk.blue,
95
- accepted: chalk.green,
96
- ignored: chalk.yellow,
97
- blocked: chalk.red,
98
- }
@@ -1,51 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { getLeaderboard } from '../api/users.ts'
4
-
5
- interface LeaderboardOptions {
6
- rising?: boolean
7
- json?: boolean
8
- }
9
-
10
- export async function leaderboardCommand(options: LeaderboardOptions): Promise<void> {
11
- const label = options.rising ? 'Rising developers this week' : 'Most pinged developers'
12
- const spinner = ora(`Loading ${label.toLowerCase()}…`).start()
13
-
14
- let entries
15
- try {
16
- entries = await getLeaderboard(options.rising)
17
- } catch (err) {
18
- spinner.fail('Failed to load leaderboard')
19
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
20
- }
21
-
22
- spinner.stop()
23
-
24
- if (options.json) {
25
- console.log(JSON.stringify(entries))
26
- return
27
- }
28
-
29
- console.log()
30
- console.log(chalk.bold(` ${label}`))
31
- console.log(chalk.dim(' ─────────────────────────────────────────'))
32
- console.log()
33
-
34
- if (entries.length === 0) {
35
- console.log(chalk.dim(' No data yet.'))
36
- return
37
- }
38
-
39
- entries.forEach((entry, idx) => {
40
- const rank = String(idx + 1).padStart(2)
41
- const score = String(entry.rep_score).padStart(3)
42
- const tier = entry.rep_tier.padEnd(6)
43
- console.log(
44
- ` ${chalk.dim(rank + '.')} ${chalk.bold('@' + entry.github_username.padEnd(20))} ` +
45
- `${chalk.cyan(score + ' rep')} ${chalk.yellow('[' + tier + ']')} ` +
46
- `${chalk.dim(entry.ping_count + ' pings')}`,
47
- )
48
- })
49
-
50
- console.log()
51
- }
@@ -1,117 +0,0 @@
1
- import http from 'http'
2
- import chalk from 'chalk'
3
- import ora from 'ora'
4
- import { createClient } from '../api/client.ts'
5
- import { setToken } from '../auth/keychain.ts'
6
- import { setUsername, getGithubClientId } from '../config.ts'
7
- import { generateCodeVerifier, generateCodeChallenge, generateState } from '../auth/pkce.ts'
8
-
9
- interface ExchangeResponse {
10
- token: string
11
- username: string
12
- }
13
-
14
- interface LoginOptions {
15
- json?: boolean
16
- }
17
-
18
- export async function loginCommand(options: LoginOptions): Promise<void> {
19
- const clientId = getGithubClientId()
20
- if (!clientId) {
21
- console.error(chalk.red('Error: GITHUB_CLIENT_ID is not configured.'))
22
- console.error(
23
- chalk.dim('Set GITHUB_CLIENT_ID env var or run: gitping config set githubClientId <id>'),
24
- )
25
- throw Object.assign(new Error('GITHUB_CLIENT_ID not configured'), { exitCode: 1 })
26
- }
27
-
28
- const codeVerifier = generateCodeVerifier()
29
- const codeChallenge = generateCodeChallenge(codeVerifier)
30
- const state = generateState()
31
-
32
- const callbackPort = 3456
33
- const redirectUri = `http://127.0.0.1:${callbackPort}/callback`
34
-
35
- // Start local callback server
36
- const server = await new Promise<http.Server>((resolve, reject) => {
37
- const s = http.createServer()
38
- s.listen(callbackPort, '127.0.0.1', () => resolve(s))
39
- s.on('error', reject)
40
- })
41
-
42
- const callbackPromise = new Promise<{ code: string; state: string }>((resolve, reject) => {
43
- server.on('request', (req, res) => {
44
- const url = new URL(req.url ?? '/', `http://localhost`)
45
- const code = url.searchParams.get('code')
46
- const st = url.searchParams.get('state')
47
- const error = url.searchParams.get('error')
48
-
49
- res.writeHead(200, { 'Content-Type': 'text/html' })
50
- res.end('<html><body><h2>Login successful — you can close this tab.</h2></body></html>')
51
- server.close()
52
-
53
- if (error || !code || !st) {
54
- reject(new Error(error ?? 'OAuth callback missing required parameters'))
55
- } else {
56
- resolve({ code, state: st })
57
- }
58
- })
59
- })
60
-
61
- const authUrl = new URL('https://github.com/login/oauth/authorize')
62
- authUrl.searchParams.set('client_id', clientId)
63
- authUrl.searchParams.set('redirect_uri', redirectUri)
64
- authUrl.searchParams.set('scope', 'read:user')
65
- authUrl.searchParams.set('state', state)
66
- authUrl.searchParams.set('code_challenge', codeChallenge)
67
- authUrl.searchParams.set('code_challenge_method', 'S256')
68
-
69
- console.log(chalk.cyan('Opening browser for GitHub login…'))
70
- console.log(chalk.dim(`If the browser does not open, visit:\n ${authUrl.toString()}`))
71
-
72
- const { default: open } = await import('open')
73
- await open(authUrl.toString())
74
-
75
- const spinner = ora('Waiting for GitHub authorization…').start()
76
-
77
- let callbackResult: { code: string; state: string }
78
- try {
79
- callbackResult = await callbackPromise
80
- } catch (err) {
81
- spinner.fail('GitHub authorization failed')
82
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 2 })
83
- }
84
-
85
- if (callbackResult.state !== state) {
86
- spinner.fail('State mismatch — possible CSRF attack. Aborting.')
87
- throw Object.assign(new Error('OAuth state mismatch'), { exitCode: 2 })
88
- }
89
-
90
- spinner.text = 'Exchanging code for token…'
91
-
92
- const client = await createClient()
93
- let result: ExchangeResponse
94
- try {
95
- result = await client
96
- .post('v1/auth/cli/exchange', {
97
- json: {
98
- code: callbackResult.code,
99
- code_verifier: codeVerifier,
100
- redirect_uri: redirectUri,
101
- },
102
- })
103
- .json<ExchangeResponse>()
104
- } catch (err) {
105
- spinner.fail('Token exchange failed')
106
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 2 })
107
- }
108
-
109
- await setToken(result.token)
110
- setUsername(result.username)
111
-
112
- spinner.succeed(chalk.green(`Logged in as ${chalk.bold('@' + result.username)}`))
113
-
114
- if (options.json) {
115
- console.log(JSON.stringify({ username: result.username }))
116
- }
117
- }
@@ -1,29 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { createClient } from '../api/client.ts'
4
- import { deleteToken } from '../auth/keychain.ts'
5
- import { clearConfig } from '../config.ts'
6
-
7
- interface LogoutOptions {
8
- json?: boolean
9
- }
10
-
11
- export async function logoutCommand(options: LogoutOptions): Promise<void> {
12
- const spinner = ora('Logging out…').start()
13
-
14
- try {
15
- const client = await createClient()
16
- await client.post('v1/auth/logout').json()
17
- } catch {
18
- // Best-effort server-side logout — we still clear local credentials
19
- }
20
-
21
- await deleteToken()
22
- clearConfig()
23
-
24
- spinner.succeed('Logged out successfully')
25
-
26
- if (options.json) {
27
- console.log(JSON.stringify({ success: true }))
28
- }
29
- }
@@ -1,57 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { sendPing } from '../api/pings.ts'
4
- import { getToken } from '../auth/keychain.ts'
5
- import { PingCategory } from '@gitping/shared'
6
-
7
- interface PingOptions {
8
- cat?: string
9
- json?: boolean
10
- }
11
-
12
- const VALID_CATEGORIES = Object.values(PingCategory) as string[]
13
-
14
- export async function pingCommand(
15
- rawRecipient: string,
16
- message: string,
17
- options: PingOptions,
18
- ): Promise<void> {
19
- const token = await getToken()
20
- if (!token) {
21
- console.error(chalk.red('Not logged in. Run `gitping login` first.'))
22
- throw Object.assign(new Error('Not authenticated'), { exitCode: 2 })
23
- }
24
-
25
- // Strip leading @ if present
26
- const recipient = rawRecipient.startsWith('@') ? rawRecipient.slice(1) : rawRecipient
27
-
28
- const category = options.cat ?? PingCategory.COLLAB
29
- if (!VALID_CATEGORIES.includes(category)) {
30
- console.error(
31
- chalk.red(`Invalid category "${category}". Valid values: ${VALID_CATEGORIES.join(', ')}`),
32
- )
33
- throw Object.assign(new Error('Invalid category'), { exitCode: 1 })
34
- }
35
-
36
- const spinner = ora(`Pinging @${recipient}…`).start()
37
-
38
- let ping
39
- try {
40
- ping = await sendPing({ recipient, message, category })
41
- } catch (err) {
42
- spinner.fail('Failed to send ping')
43
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
44
- }
45
-
46
- spinner.succeed(
47
- ping.status === 'delivered'
48
- ? chalk.green(`Ping delivered to @${recipient} (${chalk.dim(ping.id.slice(0, 8))})`)
49
- : chalk.yellow(
50
- `Ping queued for @${recipient} — they'll receive it when they join (${chalk.dim(ping.id.slice(0, 8))})`,
51
- ),
52
- )
53
-
54
- if (options.json) {
55
- console.log(JSON.stringify(ping))
56
- }
57
- }
@@ -1,64 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { getUser } from '../api/users.ts'
4
-
5
- interface SearchOptions {
6
- json?: boolean
7
- }
8
-
9
- export async function searchCommand(rawUsername: string, options: SearchOptions): Promise<void> {
10
- const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername
11
-
12
- const spinner = ora(`Searching for @${username}…`).start()
13
-
14
- let profile
15
- try {
16
- profile = await getUser(username)
17
- } catch (err) {
18
- const typed = err as { code?: string }
19
- if (typed.code === 'NOT_FOUND') {
20
- spinner.fail(`User @${username} not found on GitPing.`)
21
- throw Object.assign(new Error('User not found'), { exitCode: 1 })
22
- }
23
- spinner.fail('Search failed')
24
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
25
- }
26
-
27
- spinner.stop()
28
-
29
- if (options.json) {
30
- console.log(JSON.stringify(profile))
31
- return
32
- }
33
-
34
- console.log()
35
- console.log(chalk.bold(`@${profile.github_username}`))
36
- if (profile.bio) console.log(chalk.dim(profile.bio))
37
- console.log()
38
- console.log(` ${chalk.cyan('Availability:')} ${formatAvailability(profile.availability)}`)
39
- if (profile.availability_msg) {
40
- console.log(` ${chalk.cyan('Status:')} ${profile.availability_msg}`)
41
- }
42
- console.log(` ${chalk.cyan('Rep tier:')} ${formatTier(profile.rep_tier)}`)
43
- console.log()
44
- }
45
-
46
- function formatAvailability(mode: string): string {
47
- const map: Record<string, string> = {
48
- open_collab: chalk.green('Open to collab'),
49
- open_work: chalk.green('Open to work'),
50
- selective: chalk.yellow('Selective'),
51
- heads_down: chalk.red('Heads down'),
52
- }
53
- return map[mode] ?? mode
54
- }
55
-
56
- function formatTier(tier: string): string {
57
- const map: Record<string, string> = {
58
- LOW: chalk.red('LOW'),
59
- MEDIUM: chalk.yellow('MEDIUM'),
60
- HIGH: chalk.green('HIGH'),
61
- TOP: chalk.cyan('TOP ⭐'),
62
- }
63
- return map[tier] ?? tier
64
- }
@@ -1,116 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { getMe, updateMe } from '../api/users.ts'
4
- import { getToken } from '../auth/keychain.ts'
5
- import { Availability } from '@gitping/shared'
6
-
7
- interface StatusOptions {
8
- set?: string
9
- message?: string
10
- json?: boolean
11
- }
12
-
13
- const AVAILABILITY_DISPLAY: Record<string, string> = {
14
- open_collab: 'open-to-collab',
15
- open_work: 'open-to-work',
16
- selective: 'selective',
17
- heads_down: 'heads-down',
18
- }
19
-
20
- const AVAILABILITY_FROM_INPUT: Record<string, Availability> = {
21
- 'open-to-collab': Availability.OPEN_COLLAB,
22
- 'open-to-work': Availability.OPEN_WORK,
23
- selective: Availability.SELECTIVE,
24
- 'heads-down': Availability.HEADS_DOWN,
25
- // also accept raw enum values
26
- open_collab: Availability.OPEN_COLLAB,
27
- open_work: Availability.OPEN_WORK,
28
- heads_down: Availability.HEADS_DOWN,
29
- }
30
-
31
- export async function statusCommand(options: StatusOptions): Promise<void> {
32
- const token = await getToken()
33
- if (!token) {
34
- console.error(chalk.red('Not logged in. Run `gitping login` first.'))
35
- throw Object.assign(new Error('Not authenticated'), { exitCode: 2 })
36
- }
37
-
38
- if (options.set || options.message) {
39
- await setStatus(options)
40
- } else {
41
- await showStatus(options)
42
- }
43
- }
44
-
45
- async function showStatus(options: StatusOptions): Promise<void> {
46
- const spinner = ora('Fetching status…').start()
47
-
48
- let user
49
- try {
50
- user = await getMe()
51
- } catch (err) {
52
- spinner.fail('Failed to fetch status')
53
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
54
- }
55
-
56
- spinner.stop()
57
-
58
- if (options.json) {
59
- console.log(
60
- JSON.stringify({
61
- availability: user.availability,
62
- availability_msg: user.availability_msg,
63
- }),
64
- )
65
- return
66
- }
67
-
68
- const displayMode = AVAILABILITY_DISPLAY[user.availability] ?? user.availability
69
- console.log(`\n ${chalk.cyan('Mode:')} ${chalk.bold(displayMode)}`)
70
- if (user.availability_msg) {
71
- console.log(` ${chalk.cyan('Message:')} ${user.availability_msg}`)
72
- }
73
- console.log()
74
- }
75
-
76
- async function setStatus(options: StatusOptions): Promise<void> {
77
- const update: Record<string, string> = {}
78
-
79
- if (options.set) {
80
- const mode = AVAILABILITY_FROM_INPUT[options.set]
81
- if (!mode) {
82
- console.error(
83
- chalk.red(
84
- `Invalid mode "${options.set}". Valid values: open-to-collab, open-to-work, selective, heads-down`,
85
- ),
86
- )
87
- throw Object.assign(new Error('Invalid availability mode'), { exitCode: 1 })
88
- }
89
- update['availability'] = mode
90
- }
91
-
92
- if (options.message !== undefined) {
93
- update['availability_msg'] = options.message
94
- }
95
-
96
- const spinner = ora('Updating status…').start()
97
-
98
- let user
99
- try {
100
- user = await updateMe(update)
101
- } catch (err) {
102
- spinner.fail('Failed to update status')
103
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
104
- }
105
-
106
- spinner.succeed(chalk.green('Status updated.'))
107
-
108
- if (options.json) {
109
- console.log(
110
- JSON.stringify({
111
- availability: user.availability,
112
- availability_msg: user.availability_msg,
113
- }),
114
- )
115
- }
116
- }
@@ -1,57 +0,0 @@
1
- import chalk from 'chalk'
2
- import ora from 'ora'
3
- import { getMe } from '../api/users.ts'
4
- import { getToken } from '../auth/keychain.ts'
5
-
6
- interface WhoamiOptions {
7
- json?: boolean
8
- }
9
-
10
- export async function whoamiCommand(options: WhoamiOptions): Promise<void> {
11
- const token = await getToken()
12
- if (!token) {
13
- console.error(chalk.red('Not logged in. Run `gitping login` first.'))
14
- throw Object.assign(new Error('Not authenticated'), { exitCode: 2 })
15
- }
16
-
17
- const spinner = ora('Fetching profile…').start()
18
-
19
- let user
20
- try {
21
- user = await getMe()
22
- } catch (err) {
23
- spinner.fail('Failed to fetch profile')
24
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { exitCode: 3 })
25
- }
26
-
27
- spinner.stop()
28
-
29
- if (options.json) {
30
- console.log(JSON.stringify(user))
31
- return
32
- }
33
-
34
- console.log()
35
- console.log(chalk.bold(`@${user.github_username}`))
36
- if (user.bio) console.log(chalk.dim(user.bio))
37
- console.log()
38
- console.log(` ${chalk.cyan('Availability:')} ${formatAvailability(user.availability)}`)
39
- if (user.availability_msg) {
40
- console.log(` ${chalk.cyan('Status:')} ${user.availability_msg}`)
41
- }
42
- console.log(` ${chalk.cyan('Rep score:')} ${user.rep_score}/100`)
43
- if (user.suspended) {
44
- console.log(` ${chalk.red('⚠ Account suspended')}`)
45
- }
46
- console.log()
47
- }
48
-
49
- function formatAvailability(mode: string): string {
50
- const map: Record<string, string> = {
51
- open_collab: chalk.green('Open to collab'),
52
- open_work: chalk.green('Open to work'),
53
- selective: chalk.yellow('Selective'),
54
- heads_down: chalk.red('Heads down'),
55
- }
56
- return map[mode] ?? mode
57
- }
package/src/config.ts DELETED
@@ -1,39 +0,0 @@
1
- import Conf from 'conf'
2
-
3
- interface CliConfig {
4
- apiBaseUrl: string
5
- githubClientId: string
6
- username: string | undefined
7
- }
8
-
9
- const DEFAULT_API_BASE_URL = 'https://gitping.onrender.com'
10
- const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lidHtFLHfZozhh8w'
11
-
12
- export const conf = new Conf<CliConfig>({
13
- projectName: 'gitping',
14
- defaults: {
15
- apiBaseUrl: DEFAULT_API_BASE_URL,
16
- githubClientId: DEFAULT_GITHUB_CLIENT_ID,
17
- username: undefined,
18
- },
19
- })
20
-
21
- export function getApiBaseUrl(): string {
22
- return (process.env['GITPING_API_URL'] ?? conf.get('apiBaseUrl')).replace(/\/$/, '')
23
- }
24
-
25
- export function getGithubClientId(): string {
26
- return process.env['GITHUB_CLIENT_ID'] ?? conf.get('githubClientId')
27
- }
28
-
29
- export function getUsername(): string | undefined {
30
- return conf.get('username')
31
- }
32
-
33
- export function setUsername(username: string): void {
34
- conf.set('username', username)
35
- }
36
-
37
- export function clearConfig(): void {
38
- conf.clear()
39
- }