gitpal-cli 1.0.4 → 1.0.5

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 CHANGED
@@ -157,7 +157,7 @@ gitpal changelog --ver 2.0.0
157
157
 
158
158
  # šŸ“„ CHANGELOG v2.0.0:
159
159
  #
160
- # ## [2.0.0] - 2026-03-21
160
+ # ## [2.0.0] - 2026-03-22
161
161
  #
162
162
  # ### Features
163
163
  # - Payment gateway integration
@@ -223,6 +223,61 @@ gitpal review --review-only # Only review, skip commit step
223
223
 
224
224
  ---
225
225
 
226
+ ### `gitpal explain` — Explain Any Code
227
+ Explains any file, function or commit in plain English — perfect for understanding old code or teammate's changes.
228
+
229
+ ```bash
230
+ # Explain an entire file
231
+ gitpal explain src/auth.js
232
+
233
+ # šŸ“– Explaining file: auth.js
234
+ # ──────────────────────────────────────────────────
235
+ # This file handles all authentication logic.
236
+ # It has 4 main functions:
237
+ # - login() — verifies user credentials
238
+ # - register() — creates new user account
239
+ # - verifyToken() — checks if JWT is valid
240
+ # - logout() — clears user session
241
+ #
242
+ # Depends on: bcrypt, jsonwebtoken, User model
243
+ ```
244
+
245
+ ```bash
246
+ # Explain a specific function
247
+ gitpal explain src/payment.js --function processPayment
248
+
249
+ # šŸ“– Explaining function: processPayment()
250
+ # ──────────────────────────────────────────────────
251
+ # This function handles Razorpay payment processing.
252
+ # Step 1 — Creates payment order with amount
253
+ # Step 2 — Sends to Razorpay API
254
+ # Step 3 — Waits for webhook confirmation
255
+ # Step 4 — Updates database on success
256
+ #
257
+ # Depends on: razorpay, axios, Order model
258
+ ```
259
+
260
+ ```bash
261
+ # Explain any commit
262
+ gitpal explain a3f2c1
263
+
264
+ # šŸ“– Explaining commit: a3f2c1
265
+ # ──────────────────────────────────────────────────
266
+ # This commit added JWT authentication.
267
+ # - Created login function with bcrypt password check
268
+ # - Added JWT token generation on success
269
+ # - Protected private routes with middleware
270
+ ```
271
+
272
+ **Options:**
273
+ ```bash
274
+ gitpal explain src/auth.js # Explain full file
275
+ gitpal explain src/auth.js --function login # Explain one function
276
+ gitpal explain a3f2c1 # Explain a commit
277
+ ```
278
+
279
+ ---
280
+
226
281
  ## šŸ”„ Full Daily Workflow
227
282
 
228
283
  ```
@@ -248,6 +303,9 @@ gitpal pr → Full PR description, copy to GitHub
248
303
  ↓
249
304
  Releasing v2.0?
250
305
  gitpal changelog --ver 2.0.0 → Full changelog ready
306
+ ↓
307
+ Understanding old code?
308
+ gitpal explain src/auth.js → Plain English explanation
251
309
  ```
252
310
 
253
311
  ---
@@ -261,6 +319,7 @@ gitpal changelog --ver 2.0.0 → Full changelog ready
261
319
  | Forget what you built last week | Plain English summary instantly |
262
320
  | Write changelog manually | Auto-generated from commits |
263
321
  | No code review before commit | AI catches bugs before they reach GitHub |
322
+ | Confused by old code | Explained in plain English instantly |
264
323
  | Works with one AI only | Works with 4 AI providers |
265
324
 
266
325
  ---
@@ -281,7 +340,8 @@ gitpal/
281
340
  │ ā”œā”€ā”€ pr.js ← gitpal pr
282
341
  │ ā”œā”€ā”€ changelog.js ← gitpal changelog
283
342
  │ ā”œā”€ā”€ config.js ← gitpal config
284
- │ └── review.js ← gitpal review
343
+ │ ā”œā”€ā”€ review.js ← gitpal review
344
+ │ └── explain.js ← gitpal explain
285
345
  └── tests/
286
346
  └── ai.test.js
287
347
  ```
@@ -339,4 +399,4 @@ MIT — free to use, modify and distribute.
339
399
 
340
400
  <p align="center">
341
401
  <strong>If GitPal saves you time, give it a ⭐ on GitHub!</strong>
342
- </p>
402
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpal-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "AI-powered Git assistant CLI",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/ai.js CHANGED
@@ -2,7 +2,6 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
 
5
- // Config file stored in user's home directory
6
5
  const CONFIG_PATH = path.join(os.homedir(), '.gitpal.json');
7
6
 
8
7
  export function loadConfig() {
@@ -14,23 +13,11 @@ export function saveConfig(config) {
14
13
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
15
14
  }
16
15
 
17
- // ─── AI PROVIDER ROUTER ───────────────────────────────────────────────────────
18
- // All providers receive the same prompt and return a plain string response.
19
- // Adding a new provider = add one function + one case below.
20
-
21
16
  async function callAnthropic(prompt, apiKey) {
22
17
  const res = await fetch('https://api.anthropic.com/v1/messages', {
23
18
  method: 'POST',
24
- headers: {
25
- 'Content-Type': 'application/json',
26
- 'x-api-key': apiKey,
27
- 'anthropic-version': '2023-06-01',
28
- },
29
- body: JSON.stringify({
30
- model: 'claude-3-haiku-20240307',
31
- max_tokens: 500,
32
- messages: [{ role: 'user', content: prompt }],
33
- }),
19
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
20
+ body: JSON.stringify({ model: 'claude-3-haiku-20240307', max_tokens: 500, messages: [{ role: 'user', content: prompt }] }),
34
21
  });
35
22
  const data = await res.json();
36
23
  if (!res.ok) throw new Error(data.error?.message || 'Anthropic API error');
@@ -40,15 +27,8 @@ async function callAnthropic(prompt, apiKey) {
40
27
  async function callOpenAI(prompt, apiKey) {
41
28
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
42
29
  method: 'POST',
43
- headers: {
44
- 'Content-Type': 'application/json',
45
- Authorization: `Bearer ${apiKey}`,
46
- },
47
- body: JSON.stringify({
48
- model: 'gpt-3.5-turbo',
49
- max_tokens: 500,
50
- messages: [{ role: 'user', content: prompt }],
51
- }),
30
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
31
+ body: JSON.stringify({ model: 'gpt-3.5-turbo', max_tokens: 500, messages: [{ role: 'user', content: prompt }] }),
52
32
  });
53
33
  const data = await res.json();
54
34
  if (!res.ok) throw new Error(data.error?.message || 'OpenAI API error');
@@ -60,9 +40,7 @@ async function callGemini(prompt, apiKey) {
60
40
  const res = await fetch(url, {
61
41
  method: 'POST',
62
42
  headers: { 'Content-Type': 'application/json' },
63
- body: JSON.stringify({
64
- contents: [{ parts: [{ text: prompt }] }],
65
- }),
43
+ body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }),
66
44
  });
67
45
  const data = await res.json();
68
46
  if (!res.ok) throw new Error(data.error?.message || 'Gemini API error');
@@ -72,39 +50,38 @@ async function callGemini(prompt, apiKey) {
72
50
  async function callGroq(prompt, apiKey) {
73
51
  const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
74
52
  method: 'POST',
75
- headers: {
76
- 'Content-Type': 'application/json',
77
- Authorization: `Bearer ${apiKey}`,
78
- },
79
- body: JSON.stringify({
80
- model: 'llama-3.3-70b-versatile',
81
- max_tokens: 500,
82
- messages: [{ role: 'user', content: prompt }],
83
- }),
53
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
54
+ body: JSON.stringify({ model: 'llama-3.3-70b-versatile', max_tokens: 500, messages: [{ role: 'user', content: prompt }] }),
84
55
  });
85
56
  const data = await res.json();
86
57
  if (!res.ok) throw new Error(data.error?.message || 'Groq API error');
87
58
  return data.choices[0].message.content.trim();
88
59
  }
89
60
 
90
- // ─── MAIN EXPORT ─────────────────────────────────────────────────────────────
61
+ async function callOpenRouter(prompt, apiKey, model) {
62
+ const selectedModel = model || 'google/gemini-2.0-flash-exp:free';
63
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': 'https://github.com/h1a2r3s4h/gitpal', 'X-Title': 'GitPal CLI' },
66
+ body: JSON.stringify({ model: selectedModel, max_tokens: 500, messages: [{ role: 'user', content: prompt }] }),
67
+ });
68
+ const data = await res.json();
69
+ if (!res.ok) throw new Error(data.error?.message || 'OpenRouter API error');
70
+ return data.choices[0].message.content.trim();
71
+ }
91
72
 
92
73
  export async function askAI(prompt) {
93
74
  const config = loadConfig();
94
-
95
75
  const provider = config.provider;
96
76
  const apiKey = config.apiKey;
97
-
98
- if (!provider || !apiKey) {
99
- throw new Error('No AI provider configured. Run: gitpal config');
100
- }
101
-
77
+ const model = config.model;
78
+ if (!provider || !apiKey) throw new Error('No AI provider configured. Run: gitpal config');
102
79
  switch (provider) {
103
- case 'anthropic': return callAnthropic(prompt, apiKey);
104
- case 'openai': return callOpenAI(prompt, apiKey);
105
- case 'gemini': return callGemini(prompt, apiKey);
106
- case 'groq': return callGroq(prompt, apiKey);
107
- default:
108
- throw new Error(`Unknown provider "${provider}". Run: gitpal config`);
80
+ case 'anthropic': return callAnthropic(prompt, apiKey);
81
+ case 'openai': return callOpenAI(prompt, apiKey);
82
+ case 'gemini': return callGemini(prompt, apiKey);
83
+ case 'groq': return callGroq(prompt, apiKey);
84
+ case 'openrouter': return callOpenRouter(prompt, apiKey, model);
85
+ default: throw new Error(`Unknown provider "${provider}". Run: gitpal config`);
109
86
  }
110
87
  }
@@ -19,6 +19,7 @@ const PROVIDERS = {
19
19
  label: "Groq — llama3-8b (Free & Ultra Fast)",
20
20
  keyHint: "Get free API key at console.groq.com",
21
21
  },
22
+
22
23
  };
23
24
 
24
25
  export async function configCommand() {
@@ -0,0 +1,291 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import inquirer from 'inquirer';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import simpleGit from 'simple-git';
7
+ import { isGitRepo, getCurrentBranch } from '../git.js';
8
+ import { askAI, loadConfig, saveConfig } from '../ai.js';
9
+
10
+ const git = simpleGit();
11
+
12
+ // ─── GITHUB API ───────────────────────────────────────────────────────────────
13
+
14
+ async function fetchIssue(repo, issueNumber, token) {
15
+ const url = `https://api.github.com/repos/${repo}/issues/${issueNumber}`;
16
+ const headers = {
17
+ 'Accept': 'application/vnd.github.v3+json',
18
+ 'User-Agent': 'gitpal-cli',
19
+ };
20
+ if (token) headers['Authorization'] = `token ${token}`;
21
+
22
+ const res = await fetch(url, { headers });
23
+ if (!res.ok) {
24
+ if (res.status === 404) throw new Error(`Issue #${issueNumber} not found in ${repo}`);
25
+ if (res.status === 401) throw new Error('Invalid GitHub token. Run: gitpal config --github-token YOUR_TOKEN');
26
+ throw new Error(`GitHub API error: ${res.status}`);
27
+ }
28
+ return res.json();
29
+ }
30
+
31
+ async function fetchRepoFiles(repo, token) {
32
+ const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
33
+ const headers = {
34
+ 'Accept': 'application/vnd.github.v3+json',
35
+ 'User-Agent': 'gitpal-cli',
36
+ };
37
+ if (token) headers['Authorization'] = `token ${token}`;
38
+
39
+ const res = await fetch(url, { headers });
40
+ if (!res.ok) return [];
41
+ const data = await res.json();
42
+ return data.tree?.filter(f => f.type === 'blob').map(f => f.path) || [];
43
+ }
44
+
45
+ // ─── READ LOCAL FILES ─────────────────────────────────────────────────────────
46
+
47
+ function readLocalFiles(maxFiles = 10) {
48
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go'];
49
+ const ignore = ['node_modules', '.git', 'dist', 'build', 'coverage'];
50
+
51
+ const files = [];
52
+
53
+ function walk(dir) {
54
+ if (files.length >= maxFiles) return;
55
+ try {
56
+ const entries = fs.readdirSync(dir);
57
+ for (const entry of entries) {
58
+ if (ignore.includes(entry)) continue;
59
+ const fullPath = path.join(dir, entry);
60
+ const stat = fs.statSync(fullPath);
61
+ if (stat.isDirectory()) {
62
+ walk(fullPath);
63
+ } else if (extensions.includes(path.extname(entry))) {
64
+ try {
65
+ const content = fs.readFileSync(fullPath, 'utf-8');
66
+ files.push({ path: fullPath, content: content.slice(0, 1000) });
67
+ if (files.length >= maxFiles) return;
68
+ } catch {}
69
+ }
70
+ }
71
+ } catch {}
72
+ }
73
+
74
+ walk(process.cwd());
75
+ return files;
76
+ }
77
+
78
+ // ─── MAIN COMMAND ─────────────────────────────────────────────────────────────
79
+
80
+ export async function issueCommand(issueNumber, options) {
81
+ if (!(await isGitRepo())) {
82
+ console.log(chalk.red('āŒ Not a git repository. Clone the repo first.'));
83
+ process.exit(1);
84
+ }
85
+
86
+ const config = loadConfig();
87
+
88
+ // Get GitHub token
89
+ let token = options.githubToken || config.githubToken;
90
+ if (!token) {
91
+ console.log(chalk.yellow('āš ļø No GitHub token found. Using public API (rate limited).'));
92
+ console.log(chalk.dim('Add token: gitpal config --github-token YOUR_TOKEN\n'));
93
+ }
94
+
95
+ // Get repo
96
+ let repo = options.repo;
97
+ if (!repo) {
98
+ try {
99
+ const remotes = await git.getRemotes(true);
100
+ const origin = remotes.find(r => r.name === 'origin');
101
+ if (origin) {
102
+ const match = origin.refs.fetch.match(/github\.com[:/](.+?)(?:\.git)?$/);
103
+ if (match) repo = match[1];
104
+ }
105
+ } catch {}
106
+ }
107
+
108
+ if (!repo) {
109
+ console.log(chalk.red('āŒ Could not detect repo. Use: gitpal issue 234 --repo owner/repo'));
110
+ process.exit(1);
111
+ }
112
+
113
+ console.log(chalk.dim(`\nRepo: ${repo} | Issue: #${issueNumber}\n`));
114
+
115
+ // ── Step 1: Fetch Issue ──────────────────────────────────────────────────
116
+
117
+ const issueSpinner = ora(`Fetching issue #${issueNumber} from GitHub...`).start();
118
+ let issue;
119
+ try {
120
+ issue = await fetchIssue(repo, issueNumber, token);
121
+ issueSpinner.succeed(`Issue found: "${issue.title}"`);
122
+ } catch (err) {
123
+ issueSpinner.fail(chalk.red(`Failed: ${err.message}`));
124
+ process.exit(1);
125
+ }
126
+
127
+ // ── Step 2: Read local codebase ──────────────────────────────────────────
128
+
129
+ const codeSpinner = ora('Reading your local codebase...').start();
130
+ const localFiles = readLocalFiles(10);
131
+ codeSpinner.succeed(`Read ${localFiles.length} files from codebase.`);
132
+
133
+ // ── Step 3: AI Analysis ──────────────────────────────────────────────────
134
+
135
+ const aiSpinner = ora('AI is analyzing the issue and your codebase...').start();
136
+
137
+ const codeContext = localFiles
138
+ .map(f => `File: ${f.path}\n${f.content}`)
139
+ .join('\n\n---\n\n');
140
+
141
+ const prompt = `You are a senior developer helping a junior developer fix a GitHub issue.
142
+
143
+ GITHUB ISSUE:
144
+ Title: ${issue.title}
145
+ Description: ${issue.body || 'No description'}
146
+ Labels: ${issue.labels?.map(l => l.name).join(', ') || 'None'}
147
+
148
+ LOCAL CODEBASE (first 10 files):
149
+ ${codeContext.slice(0, 5000)}
150
+
151
+ Based on the issue and codebase, provide:
152
+
153
+ 1. UNDERSTANDING (2-3 lines explaining the bug simply)
154
+
155
+ 2. FILES TO CHANGE (list exact file paths that need changes)
156
+
157
+ 3. HOW TO FIX (step by step, simple language, include code snippets)
158
+
159
+ 4. DIFFICULTY: Easy / Medium / Hard
160
+
161
+ 5. ESTIMATED TIME: X minutes/hours
162
+
163
+ 6. COMMIT MESSAGE (conventional commit format)
164
+
165
+ 7. PR TITLE (clear and professional)
166
+
167
+ 8. PR DESCRIPTION (What changed, Why, How to test)
168
+
169
+ Be specific and practical. Junior developers should understand this.`;
170
+
171
+ let analysis;
172
+ try {
173
+ analysis = await askAI(prompt);
174
+ aiSpinner.succeed('Analysis complete!\n');
175
+ } catch (err) {
176
+ aiSpinner.fail(chalk.red(`AI Error: ${err.message}`));
177
+ process.exit(1);
178
+ }
179
+
180
+ // ── Step 4: Display Analysis ─────────────────────────────────────────────
181
+
182
+ console.log(chalk.bold.cyan(`\nšŸ” Issue #${issueNumber}: ${issue.title}\n`));
183
+ console.log(chalk.dim('─'.repeat(60)));
184
+ console.log(chalk.white(analysis));
185
+ console.log(chalk.dim('─'.repeat(60)));
186
+
187
+ // ── Step 5: Ask what to do ───────────────────────────────────────────────
188
+
189
+ const { action } = await inquirer.prompt([{
190
+ type: 'list',
191
+ name: 'action',
192
+ message: '\nWhat would you like to do?',
193
+ choices: [
194
+ { name: '🌿 Create a new branch for this fix', value: 'branch' },
195
+ { name: 'šŸ“‹ Generate full PR description', value: 'pr' },
196
+ { name: 'šŸ‘€ I will fix it manually', value: 'manual' },
197
+ { name: 'āŒ Exit', value: 'exit' },
198
+ ],
199
+ }]);
200
+
201
+ if (action === 'exit' || action === 'manual') {
202
+ console.log(chalk.yellow('\nGood luck with the fix! Run gitpal commit when done.'));
203
+ return;
204
+ }
205
+
206
+ // ── Step 6: Create Branch ────────────────────────────────────────────────
207
+
208
+ if (action === 'branch' || action === 'pr') {
209
+ const branchName = `fix/issue-${issueNumber}-${issue.title
210
+ .toLowerCase()
211
+ .replace(/[^a-z0-9\s]/g, '')
212
+ .replace(/\s+/g, '-')
213
+ .slice(0, 30)}`;
214
+
215
+ const { confirmBranch } = await inquirer.prompt([{
216
+ type: 'confirm',
217
+ name: 'confirmBranch',
218
+ message: `Create branch: ${chalk.cyan(branchName)}?`,
219
+ default: true,
220
+ }]);
221
+
222
+ if (confirmBranch) {
223
+ const branchSpinner = ora('Creating branch...').start();
224
+ try {
225
+ await git.checkoutLocalBranch(branchName);
226
+ branchSpinner.succeed(chalk.green(`Branch created: ${branchName}`));
227
+ } catch (err) {
228
+ branchSpinner.fail(chalk.red(`Branch error: ${err.message}`));
229
+ }
230
+ }
231
+ }
232
+
233
+ // ── Step 7: Generate PR Description ─────────────────────────────────────
234
+
235
+ if (action === 'pr') {
236
+ const prSpinner = ora('Generating PR description...').start();
237
+
238
+ const prPrompt = `Write a professional GitHub Pull Request description for fixing issue #${issueNumber}.
239
+
240
+ Issue Title: ${issue.title}
241
+ Issue Description: ${issue.body?.slice(0, 500) || 'No description'}
242
+
243
+ Format exactly like this:
244
+ ## Fixes
245
+ Closes #${issueNumber}
246
+
247
+ ## What changed
248
+ (bullet points)
249
+
250
+ ## Why
251
+ (brief reason)
252
+
253
+ ## How to test
254
+ (testing steps)
255
+
256
+ ## Type of change
257
+ (Bug fix / Feature / etc)`;
258
+
259
+ try {
260
+ const prDesc = await askAI(prPrompt);
261
+ prSpinner.succeed('PR description ready!\n');
262
+ console.log(chalk.bold('\nšŸ“ Pull Request Description:\n'));
263
+ console.log(chalk.dim('─'.repeat(60)));
264
+ console.log(chalk.white(prDesc));
265
+ console.log(chalk.dim('─'.repeat(60)));
266
+ console.log(chalk.dim('\nšŸ’” Copy the above and paste into your GitHub PR.\n'));
267
+ } catch (err) {
268
+ prSpinner.fail(chalk.red(`Error: ${err.message}`));
269
+ }
270
+ }
271
+
272
+ // ── Step 8: Final instructions ───────────────────────────────────────────
273
+
274
+ console.log(chalk.bold.green('\nāœ… You are ready to contribute!\n'));
275
+ console.log(chalk.white('Next steps:'));
276
+ console.log(chalk.cyan(' 1.') + chalk.white(' Fix the issue in your editor'));
277
+ console.log(chalk.cyan(' 2.') + chalk.white(' git add .'));
278
+ console.log(chalk.cyan(' 3.') + chalk.white(' gitpal commit'));
279
+ console.log(chalk.cyan(' 4.') + chalk.white(` git push origin fix/issue-${issueNumber}`));
280
+ console.log(chalk.cyan(' 5.') + chalk.white(' Open PR on GitHub\n'));
281
+ }
282
+
283
+ // ─── CONFIG GITHUB TOKEN ──────────────────────────────────────────────────────
284
+
285
+ export function saveGithubToken(token) {
286
+ const config = loadConfig();
287
+ config.githubToken = token;
288
+ saveConfig(config);
289
+ console.log(chalk.green('\nāœ… GitHub token saved!'));
290
+ console.log(chalk.dim('You can now use: gitpal issue 234 --repo owner/repo\n'));
291
+ }
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ import { changelogCommand } from './commands/changelog.js';
7
7
  import { configCommand } from './commands/config.js';
8
8
  import { reviewCommand } from './commands/review.js';
9
9
  import { explainCommand } from './commands/explain.js';
10
+ import { issueCommand, saveGithubToken } from './commands/issue.js';
10
11
  const program = new Command();
11
12
 
12
13
  console.log(chalk.cyan.bold('\nšŸ¤– GitPal — Your AI Git Assistant\n'));
@@ -58,6 +59,13 @@ program
58
59
  .description('Explain any file or commit in plain English')
59
60
  .option('-f, --function <name>', 'Explain a specific function')
60
61
  .action(explainCommand);
62
+
63
+ program
64
+ .command('issue <number>')
65
+ .description('Fetch and fix any GitHub issue with AI guidance')
66
+ .option('-r, --repo <repo>', 'GitHub repo (owner/repo)')
67
+ .option('-t, --github-token <token>', 'GitHub personal access token')
68
+ .action(issueCommand);
61
69
  program.parse(process.argv);
62
70
 
63
71
  // Show help if no command given