linear-github-cli 1.0.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.
- package/.env.example +3 -0
- package/LICENSE +15 -0
- package/README.md +336 -0
- package/dist/branch-utils.js +270 -0
- package/dist/cli.js +43 -0
- package/dist/commands/commit-first.js +88 -0
- package/dist/commands/create-parent.js +160 -0
- package/dist/commands/create-sub.js +174 -0
- package/dist/github-client.js +123 -0
- package/dist/input-handler.js +190 -0
- package/dist/lgcmf.js +13 -0
- package/dist/linear-client.js +433 -0
- package/dist/validate.js +34 -0
- package/package.json +56 -0
package/.env.example
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 negoth
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Linear + GitHub CLI Tool
|
|
2
|
+
|
|
3
|
+
A CLI tool for creating GitHub issues with Linear integration, providing an interactive experience with autocomplete and dropdown selections.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Option 1: Install from npm (Recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g linear-github-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After installation, use `lg` or `linear-github` from anywhere:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
lg create-parent
|
|
17
|
+
lg create-sub
|
|
18
|
+
lg --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Option 2: Install from Source
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone <repository-url>
|
|
25
|
+
cd linear-github-cli
|
|
26
|
+
npm install
|
|
27
|
+
npm install -g .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Option 3: Development Mode
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone <repository-url>
|
|
34
|
+
cd linear-github-cli
|
|
35
|
+
npm install
|
|
36
|
+
npm run dev create-parent
|
|
37
|
+
npm run dev create-sub
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
### 1. Install Dependencies (if installing from source)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Configure Environment Variables
|
|
49
|
+
|
|
50
|
+
**Option 1: Using .env file (Recommended)**
|
|
51
|
+
|
|
52
|
+
Create a `.env` file in the project root:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cp .env.example .env
|
|
56
|
+
# Then edit .env and add your API key: LINEAR_API_KEY=lin_api_...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or create it directly:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
echo 'LINEAR_API_KEY=lin_api_...' > .env
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Option 2: Export in shell (Temporary)**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export LINEAR_API_KEY="lin_api_..."
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Get your Linear API key:**
|
|
72
|
+
1. Go to [Linear Settings > API](https://linear.app/settings/api)
|
|
73
|
+
2. Create a new Personal API Key
|
|
74
|
+
3. Copy the key (starts with `lin_api_`)
|
|
75
|
+
|
|
76
|
+
**Note:** The `.env` file is already in `.gitignore`, so it won't be committed to Git.
|
|
77
|
+
|
|
78
|
+
### 3. Authenticate GitHub CLI
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gh auth login
|
|
82
|
+
gh auth status # Verify
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### Create Parent Issue
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
lg create-parent
|
|
91
|
+
# or
|
|
92
|
+
lg parent
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Follow the interactive prompts:
|
|
96
|
+
1. Select repository from dropdown
|
|
97
|
+
2. Enter issue title
|
|
98
|
+
3. Enter description (opens in editor)
|
|
99
|
+
4. Optionally set due date (YYYY-MM-DD)
|
|
100
|
+
5. Select GitHub labels (checkboxes). Choices: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `research`
|
|
101
|
+
6. Optionally select GitHub project
|
|
102
|
+
7. Optionally select Linear project (after sync)
|
|
103
|
+
|
|
104
|
+
### Create Sub-Issue
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
lg create-sub
|
|
108
|
+
# or
|
|
109
|
+
lg sub
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Follow the interactive prompts:
|
|
113
|
+
1. Select repository from dropdown
|
|
114
|
+
2. Select parent issue from list
|
|
115
|
+
3. Enter sub-issue title
|
|
116
|
+
4. Enter description (opens in editor)
|
|
117
|
+
5. Optionally set due date (YYYY-MM-DD)
|
|
118
|
+
6. Select GitHub labels (same predefined list as above)
|
|
119
|
+
7. Optionally select Linear project (after sync)
|
|
120
|
+
|
|
121
|
+
### Show Help
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
lg --help
|
|
125
|
+
lg create-parent --help
|
|
126
|
+
lg create-sub --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Features
|
|
130
|
+
|
|
131
|
+
- ✅ **Interactive repository selection**: Choose from accessible GitHub repositories
|
|
132
|
+
- ✅ **Project autocomplete**: Select GitHub and Linear projects from dropdowns
|
|
133
|
+
- ✅ **Parent issue selection**: Browse and select parent issues when creating sub-issues
|
|
134
|
+
- ✅ **GitHub label sync**: Multi-select from the seven standard labels (feat, fix, chore, docs, refactor, test, research); selections are mirrored to matching Linear team labels
|
|
135
|
+
- ✅ **Due date input**: Optional date picker with validation
|
|
136
|
+
- ✅ **Automatic Linear sync**: Waits for Linear sync and updates metadata (due date, project, labels)
|
|
137
|
+
- ✅ **Parent-child relationships**: Automatically links sub-issues to parent issues
|
|
138
|
+
- ✅ **Status automation**: Issues start in Linear backlog; rely on the Linear × GitHub PR automation for status changes
|
|
139
|
+
|
|
140
|
+
## Examples
|
|
141
|
+
|
|
142
|
+
### Basic Usage
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Create a parent issue
|
|
146
|
+
lg create-parent
|
|
147
|
+
|
|
148
|
+
# Create a sub-issue
|
|
149
|
+
lg create-sub
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### With Environment Variable
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
LINEAR_API_KEY="lin_api_..." lg create-parent
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Requirements
|
|
159
|
+
|
|
160
|
+
- **Node.js** 18+ and npm
|
|
161
|
+
- **GitHub CLI** (`gh`) installed and authenticated
|
|
162
|
+
- **Linear API key** (get from [Linear Settings](https://linear.app/settings/api))
|
|
163
|
+
|
|
164
|
+
## Building from Source
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm install
|
|
168
|
+
npm run build
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The compiled JavaScript will be in the `dist/` directory.
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
### "LINEAR_API_KEY environment variable is required"
|
|
176
|
+
|
|
177
|
+
Make sure you've set the environment variable:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
export LINEAR_API_KEY="lin_api_..."
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### "lg: command not found"
|
|
184
|
+
|
|
185
|
+
If you installed globally, make sure npm's global bin directory is in your PATH:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Check npm global prefix
|
|
189
|
+
npm config get prefix
|
|
190
|
+
|
|
191
|
+
# Add to PATH (macOS/Linux)
|
|
192
|
+
export PATH="$(npm config get prefix)/bin:$PATH"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### "gh: command not found"
|
|
196
|
+
|
|
197
|
+
Install GitHub CLI:
|
|
198
|
+
- **macOS**: `brew install gh`
|
|
199
|
+
- **Linux/Windows**: See [GitHub CLI installation](https://cli.github.com/manual/installation)
|
|
200
|
+
|
|
201
|
+
### Repository list is empty
|
|
202
|
+
|
|
203
|
+
Make sure you're authenticated:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
gh auth status
|
|
207
|
+
gh auth login # if not authenticated
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Projects not showing
|
|
211
|
+
|
|
212
|
+
GitHub Projects might not be available via CLI in all repositories. This is optional - you can skip project selection.
|
|
213
|
+
|
|
214
|
+
### Linear issue not found after sync
|
|
215
|
+
|
|
216
|
+
The tool waits 5 seconds for Linear sync. If the issue still isn't found:
|
|
217
|
+
- Check Linear GitHub integration is enabled
|
|
218
|
+
- Wait a bit longer and manually update metadata in Linear
|
|
219
|
+
- The GitHub Actions workflow will also set metadata automatically
|
|
220
|
+
|
|
221
|
+
## Architecture
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
lg (CLI)
|
|
225
|
+
├── cli.ts # CLI entry point (Commander.js)
|
|
226
|
+
├── commands/
|
|
227
|
+
│ ├── create-parent.ts # Parent issue command
|
|
228
|
+
│ └── create-sub.ts # Sub-issue command
|
|
229
|
+
├── linear-client.ts # Linear SDK wrapper
|
|
230
|
+
├── github-client.ts # GitHub CLI/API wrapper
|
|
231
|
+
└── input-handler.ts # Interactive prompts (Inquirer.js)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
### Run in Development Mode
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npm run dev create-parent
|
|
240
|
+
npm run dev create-sub
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Build
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npm run build
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Label Behaviour
|
|
250
|
+
|
|
251
|
+
- The CLI surfaces the seven standard GitHub labels: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `research`. (Custom GitHub labels can still be added manually after creation.)
|
|
252
|
+
- When an issue syncs to Linear, the CLI ensures team-scoped Linear labels exist. New labels are created for the linked team if necessary and then attached to the issue.
|
|
253
|
+
- Linear issues are always created in the backlog ("No status"). Move them forward by opening PRs and letting the Linear × GitHub integration handle status updates automatically.
|
|
254
|
+
|
|
255
|
+
## Future Enhancements
|
|
256
|
+
|
|
257
|
+
- Caching of repositories and projects
|
|
258
|
+
- Configuration file for defaults
|
|
259
|
+
- Better error handling and retry logic
|
|
260
|
+
- Additional commands (list, update, close issues)
|
|
261
|
+
- Template support for issue creation
|
|
262
|
+
|
|
263
|
+
## PR Creation Workflow
|
|
264
|
+
|
|
265
|
+
After creating issues with `lg`, use the Linear-GitHub integration workflow to manage PRs and track progress.
|
|
266
|
+
|
|
267
|
+
### Recommended Approach: Aliases
|
|
268
|
+
|
|
269
|
+
The simplest approach is to use `gh` aliases or interactive mode:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Set up aliases (optional)
|
|
273
|
+
gh alias set prd 'pr create --draft --title "$1" --body "solve: #$2"'
|
|
274
|
+
gh alias set prms 'pr merge --squash --delete-branch'
|
|
275
|
+
|
|
276
|
+
# Or use interactive mode (recommended)
|
|
277
|
+
gh pr create --draft --fill
|
|
278
|
+
gh pr ready # Standard command, use directly when starting work
|
|
279
|
+
gh prms # Merge with squash and delete branch
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Workflow Overview
|
|
283
|
+
|
|
284
|
+
1. **Create issue** - Use `lg parent/sub` command
|
|
285
|
+
2. **Create branch** - Include issue number (e.g., `feat/LEA-123-task`)
|
|
286
|
+
3. **Create draft PR** - Right after branch creation, before work begins
|
|
287
|
+
- Include Linear issue ID in title (copy with `Cmd + .` in Linear)
|
|
288
|
+
- Include `solve: #123` or `Closes #123` in body
|
|
289
|
+
- Linear status: `Todo`
|
|
290
|
+
4. **Start work** - Begin actual development
|
|
291
|
+
5. **Mark PR ready** - Use `gh pr ready` when ready for review
|
|
292
|
+
- Linear status: `In Progress`
|
|
293
|
+
6. **Continue work** - More commits, add PR/issue comments for progress
|
|
294
|
+
7. **Merge** - When task is complete
|
|
295
|
+
- Linear status: `Done`
|
|
296
|
+
- GitHub issue: Closed automatically
|
|
297
|
+
|
|
298
|
+
### Two PR Types
|
|
299
|
+
|
|
300
|
+
**Completing PRs (Issue Completion):**
|
|
301
|
+
- Include Linear issue ID in title: `LEA-123 Implement login`
|
|
302
|
+
- Use `solve: #123` or `Closes #123` in body
|
|
303
|
+
- Merging sets Linear status to `Done` and closes GitHub issue
|
|
304
|
+
|
|
305
|
+
**Partial Progress PRs (Non-Completing):**
|
|
306
|
+
- Do NOT include Linear issue ID in title: `Add login form`
|
|
307
|
+
- Use `Ref: #123` in body
|
|
308
|
+
- Merging keeps Linear status unchanged and doesn't close GitHub issue
|
|
309
|
+
- Useful for tracking incremental work on large issues
|
|
310
|
+
|
|
311
|
+
### Linear Settings
|
|
312
|
+
|
|
313
|
+
Configure Linear's GitHub integration:
|
|
314
|
+
|
|
315
|
+
1. Linear Settings → Integrations → GitHub
|
|
316
|
+
2. Open "Pull request and commit automations"
|
|
317
|
+
3. Configure:
|
|
318
|
+
- **On draft PR open** → `Todo` ✅
|
|
319
|
+
- **On PR open (ready)** → `In Progress` ✅ (triggered by `gh pr ready`)
|
|
320
|
+
- **On PR review request** → `No Action`
|
|
321
|
+
- **On PR ready for merge** → `No Action`
|
|
322
|
+
- **On PR merge** → `Done` ✅
|
|
323
|
+
|
|
324
|
+
### Additional Notes
|
|
325
|
+
|
|
326
|
+
For branch name auto-extraction or more complex logic, you can create custom shell functions or use `gh pr create --fill` interactively, which allows you to manually enter the issue number.
|
|
327
|
+
|
|
328
|
+
For most cases, aliases or `gh pr create --fill` are simpler and sufficient.
|
|
329
|
+
|
|
330
|
+
### Documentation
|
|
331
|
+
|
|
332
|
+
See `workflow.md` for complete workflow documentation, examples, and troubleshooting.
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
ISC
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sanitizeBranchName = sanitizeBranchName;
|
|
7
|
+
exports.selectBranchPrefix = selectBranchPrefix;
|
|
8
|
+
exports.generateBranchName = generateBranchName;
|
|
9
|
+
exports.extractLinearIssueId = extractLinearIssueId;
|
|
10
|
+
exports.extractBranchPrefix = extractBranchPrefix;
|
|
11
|
+
exports.checkUnpushedCommitsOnCurrentBranch = checkUnpushedCommitsOnCurrentBranch;
|
|
12
|
+
exports.createGitBranch = createGitBranch;
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
15
|
+
/**
|
|
16
|
+
* Valid branch prefix types (must match commit_typed.sh)
|
|
17
|
+
*/
|
|
18
|
+
const VALID_BRANCH_PREFIXES = ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'research'];
|
|
19
|
+
/**
|
|
20
|
+
* Maps GitHub labels to branch prefix types
|
|
21
|
+
* Since GitHub labels are already updated to match the standard list,
|
|
22
|
+
* labels should directly map to branch prefixes.
|
|
23
|
+
* @param label - GitHub label name (should be one of: feat, fix, chore, docs, refactor, test, research)
|
|
24
|
+
* @returns Branch prefix (same as label if valid, otherwise returns label as-is)
|
|
25
|
+
*/
|
|
26
|
+
function mapLabelToBranchPrefix(label) {
|
|
27
|
+
const labelLower = label.toLowerCase();
|
|
28
|
+
// If label is already a valid branch prefix, return it as-is
|
|
29
|
+
if (VALID_BRANCH_PREFIXES.includes(labelLower)) {
|
|
30
|
+
return labelLower;
|
|
31
|
+
}
|
|
32
|
+
// Fallback: return label as-is (shouldn't happen if labels are properly configured)
|
|
33
|
+
return labelLower;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sanitizes a title string to be used as part of a git branch name
|
|
37
|
+
* - Converts to lowercase
|
|
38
|
+
* - Replaces spaces with hyphens
|
|
39
|
+
* - Removes special characters (keeps alphanumeric and hyphens)
|
|
40
|
+
* - Removes leading/trailing hyphens
|
|
41
|
+
* - Collapses multiple consecutive hyphens
|
|
42
|
+
* - Limits length to 50 characters
|
|
43
|
+
*
|
|
44
|
+
* @param title - The title to sanitize
|
|
45
|
+
* @returns Sanitized branch name portion
|
|
46
|
+
*/
|
|
47
|
+
function sanitizeBranchName(title) {
|
|
48
|
+
if (!title || title.trim().length === 0) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
// Convert to lowercase and replace spaces with hyphens
|
|
52
|
+
let sanitized = title.toLowerCase().replace(/\s+/g, '-');
|
|
53
|
+
// Remove special characters except hyphens and alphanumeric
|
|
54
|
+
sanitized = sanitized.replace(/[^a-z0-9-]/g, '');
|
|
55
|
+
// Collapse multiple consecutive hyphens
|
|
56
|
+
sanitized = sanitized.replace(/-+/g, '-');
|
|
57
|
+
// Remove leading and trailing hyphens
|
|
58
|
+
sanitized = sanitized.replace(/^-+|-+$/g, '');
|
|
59
|
+
// Limit length to 50 characters
|
|
60
|
+
if (sanitized.length > 50) {
|
|
61
|
+
sanitized = sanitized.substring(0, 50);
|
|
62
|
+
// Remove trailing hyphen if truncated
|
|
63
|
+
sanitized = sanitized.replace(/-+$/, '');
|
|
64
|
+
}
|
|
65
|
+
return sanitized;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Standard branch prefix options when no labels are available
|
|
69
|
+
*/
|
|
70
|
+
const STANDARD_PREFIXES = [
|
|
71
|
+
{ name: 'feat', value: 'feat' },
|
|
72
|
+
{ name: 'fix', value: 'fix' },
|
|
73
|
+
{ name: 'chore', value: 'chore' },
|
|
74
|
+
{ name: 'docs', value: 'docs' },
|
|
75
|
+
{ name: 'refactor', value: 'refactor' },
|
|
76
|
+
{ name: 'test', value: 'test' },
|
|
77
|
+
{ name: 'research', value: 'research' },
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Selects a branch prefix from GitHub labels
|
|
81
|
+
* - If no labels: prompts user to select from standard prefixes
|
|
82
|
+
* - If one label: maps and returns branch prefix
|
|
83
|
+
* - If multiple labels: prompts user to select one
|
|
84
|
+
*
|
|
85
|
+
* @param labels - Array of GitHub label names
|
|
86
|
+
* @returns Branch prefix string or null if user cancels
|
|
87
|
+
*/
|
|
88
|
+
async function selectBranchPrefix(labels) {
|
|
89
|
+
// Map all labels to branch prefixes
|
|
90
|
+
const mappedPrefixes = labels && labels.length > 0
|
|
91
|
+
? labels.map(label => ({
|
|
92
|
+
label,
|
|
93
|
+
prefix: mapLabelToBranchPrefix(label),
|
|
94
|
+
}))
|
|
95
|
+
: [];
|
|
96
|
+
// If no labels: prompt user to select from standard prefixes
|
|
97
|
+
if (mappedPrefixes.length === 0) {
|
|
98
|
+
const { selectedPrefix } = await inquirer_1.default.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'list',
|
|
101
|
+
name: 'selectedPrefix',
|
|
102
|
+
message: 'Select branch prefix (no labels selected):',
|
|
103
|
+
choices: STANDARD_PREFIXES,
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
return selectedPrefix;
|
|
107
|
+
}
|
|
108
|
+
// If only one label, return its mapped prefix
|
|
109
|
+
if (mappedPrefixes.length === 1) {
|
|
110
|
+
return mappedPrefixes[0].prefix;
|
|
111
|
+
}
|
|
112
|
+
// Multiple labels: prompt user to select
|
|
113
|
+
const choices = mappedPrefixes.map(({ label, prefix }) => ({
|
|
114
|
+
name: `${label} → ${prefix}`,
|
|
115
|
+
value: prefix,
|
|
116
|
+
}));
|
|
117
|
+
const { selectedPrefix } = await inquirer_1.default.prompt([
|
|
118
|
+
{
|
|
119
|
+
type: 'list',
|
|
120
|
+
name: 'selectedPrefix',
|
|
121
|
+
message: 'Select branch prefix (multiple labels selected):',
|
|
122
|
+
choices,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
return selectedPrefix;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Generates a branch name from prefix, Linear ID, and title
|
|
129
|
+
* Format: prefix/LinearID-sanitized-title
|
|
130
|
+
*
|
|
131
|
+
* @param prefix - Branch prefix (e.g., 'feat', 'docs')
|
|
132
|
+
* @param linearId - Linear issue ID (e.g., 'LEA-123')
|
|
133
|
+
* @param title - Issue title to sanitize
|
|
134
|
+
* @returns Full branch name
|
|
135
|
+
*/
|
|
136
|
+
function generateBranchName(prefix, linearId, title) {
|
|
137
|
+
const sanitizedTitle = sanitizeBranchName(title);
|
|
138
|
+
if (!sanitizedTitle) {
|
|
139
|
+
// If title is empty after sanitization, just use prefix and ID
|
|
140
|
+
return `${prefix}/${linearId}`;
|
|
141
|
+
}
|
|
142
|
+
return `${prefix}/${linearId}-${sanitizedTitle}`;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Extracts Linear issue ID from a branch name
|
|
146
|
+
* - Matches pattern: prefix/LEA-123-title or prefix/LEA-123
|
|
147
|
+
* - Uses regex to find Linear issue ID format: [A-Z]+-\d+
|
|
148
|
+
*
|
|
149
|
+
* @param branchName - Branch name (e.g., 'feat/LEA-123-implement-login')
|
|
150
|
+
* @returns Linear issue ID (e.g., 'LEA-123') or null if not found
|
|
151
|
+
*/
|
|
152
|
+
function extractLinearIssueId(branchName) {
|
|
153
|
+
if (!branchName || branchName.trim().length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// Match Linear issue ID pattern: [A-Z]+-\d+ (e.g., LEA-123)
|
|
157
|
+
const match = branchName.match(/([A-Z]+-\d+)/);
|
|
158
|
+
return match ? match[1] : null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Extracts branch prefix (commit type) from a branch name
|
|
162
|
+
* - Extracts the part before the first '/' (e.g., 'research' from 'research/LEA-75-probit-model')
|
|
163
|
+
* - Validates against VALID_BRANCH_PREFIXES
|
|
164
|
+
* - Returns 'feat' as default if prefix is not found or invalid
|
|
165
|
+
*
|
|
166
|
+
* @param branchName - Branch name (e.g., 'research/LEA-75-probit-model')
|
|
167
|
+
* @returns Branch prefix (e.g., 'research') or 'feat' as default
|
|
168
|
+
*/
|
|
169
|
+
function extractBranchPrefix(branchName) {
|
|
170
|
+
if (!branchName || branchName.trim().length === 0) {
|
|
171
|
+
return 'feat';
|
|
172
|
+
}
|
|
173
|
+
// Extract prefix before first '/'
|
|
174
|
+
const parts = branchName.split('/');
|
|
175
|
+
if (parts.length === 0 || !parts[0]) {
|
|
176
|
+
return 'feat';
|
|
177
|
+
}
|
|
178
|
+
const prefix = parts[0].toLowerCase();
|
|
179
|
+
// Validate against valid prefixes
|
|
180
|
+
if (VALID_BRANCH_PREFIXES.includes(prefix)) {
|
|
181
|
+
return prefix;
|
|
182
|
+
}
|
|
183
|
+
// Return 'feat' as default if prefix is invalid
|
|
184
|
+
return 'feat';
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Checks for unpushed commits on the current branch
|
|
188
|
+
* - Gets current branch name
|
|
189
|
+
* - Checks if remote branch exists
|
|
190
|
+
* - Counts unpushed commits
|
|
191
|
+
* - Returns information about unpushed commits
|
|
192
|
+
*
|
|
193
|
+
* @returns Object with hasUnpushed flag, count, and commit list
|
|
194
|
+
*/
|
|
195
|
+
function checkUnpushedCommitsOnCurrentBranch() {
|
|
196
|
+
try {
|
|
197
|
+
// Check if we're in a git repository
|
|
198
|
+
try {
|
|
199
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// Not in a git repository, return no unpushed commits
|
|
203
|
+
return { hasUnpushed: false, count: 0, commits: [] };
|
|
204
|
+
}
|
|
205
|
+
// Get current branch
|
|
206
|
+
const currentBranch = (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
207
|
+
if (!currentBranch) {
|
|
208
|
+
return { hasUnpushed: false, count: 0, commits: [] };
|
|
209
|
+
}
|
|
210
|
+
// Check if remote branch exists
|
|
211
|
+
try {
|
|
212
|
+
(0, child_process_1.execSync)(`git rev-parse --verify origin/${currentBranch}`, { stdio: 'pipe' });
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// Remote branch doesn't exist, no unpushed commits
|
|
216
|
+
return { hasUnpushed: false, count: 0, commits: [] };
|
|
217
|
+
}
|
|
218
|
+
// Count unpushed commits
|
|
219
|
+
const countOutput = (0, child_process_1.execSync)(`git rev-list --count origin/${currentBranch}..${currentBranch}`, { encoding: 'utf-8' }).trim();
|
|
220
|
+
const count = parseInt(countOutput, 10) || 0;
|
|
221
|
+
if (count === 0) {
|
|
222
|
+
return { hasUnpushed: false, count: 0, commits: [] };
|
|
223
|
+
}
|
|
224
|
+
// Get commit list
|
|
225
|
+
const commitsOutput = (0, child_process_1.execSync)(`git log origin/${currentBranch}..${currentBranch} --oneline`, { encoding: 'utf-8' }).trim();
|
|
226
|
+
const commits = commitsOutput ? commitsOutput.split('\n') : [];
|
|
227
|
+
return { hasUnpushed: true, count, commits };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// On error, assume no unpushed commits to avoid blocking workflow
|
|
231
|
+
return { hasUnpushed: false, count: 0, commits: [] };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Creates a git branch and switches to it
|
|
236
|
+
* - Checks if in git repository
|
|
237
|
+
* - Checks if branch already exists
|
|
238
|
+
* - Creates branch using 'git switch -c'
|
|
239
|
+
*
|
|
240
|
+
* @param branchName - Name of the branch to create
|
|
241
|
+
* @returns true if successful, false otherwise
|
|
242
|
+
*/
|
|
243
|
+
async function createGitBranch(branchName) {
|
|
244
|
+
try {
|
|
245
|
+
// Check if we're in a git repository
|
|
246
|
+
try {
|
|
247
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.log('⚠️ Not in a git repository. Branch creation skipped.');
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
// Check if branch already exists
|
|
254
|
+
try {
|
|
255
|
+
(0, child_process_1.execSync)(`git show-ref --verify --quiet refs/heads/${branchName}`, { stdio: 'pipe' });
|
|
256
|
+
console.log(`⚠️ Branch '${branchName}' already exists. Branch creation skipped.`);
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
// Branch doesn't exist, which is what we want
|
|
261
|
+
}
|
|
262
|
+
// Create and switch to branch
|
|
263
|
+
(0, child_process_1.execSync)(`git switch -c ${branchName}`, { stdio: 'inherit' });
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
console.error(`❌ Failed to create branch '${branchName}':`, error instanceof Error ? error.message : error);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const dotenv_1 = require("dotenv");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const create_parent_1 = require("./commands/create-parent");
|
|
8
|
+
const create_sub_1 = require("./commands/create-sub");
|
|
9
|
+
// Load .env file from the project root
|
|
10
|
+
// __dirname points to dist/ in compiled output, so we go up one level
|
|
11
|
+
(0, dotenv_1.config)({ path: (0, path_1.resolve)(__dirname, '..', '.env') });
|
|
12
|
+
const program = new commander_1.Command();
|
|
13
|
+
program
|
|
14
|
+
.name('lg')
|
|
15
|
+
.description('Linear + GitHub Integration CLI - Create GitHub issues with Linear sync')
|
|
16
|
+
.version('1.0.0');
|
|
17
|
+
program
|
|
18
|
+
.command('create-parent')
|
|
19
|
+
.alias('parent')
|
|
20
|
+
.description('Create a parent GitHub issue with Linear integration')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
try {
|
|
23
|
+
await (0, create_parent_1.createParentIssue)();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('❌ Error:', error instanceof Error ? error.message : error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
program
|
|
31
|
+
.command('create-sub')
|
|
32
|
+
.alias('sub')
|
|
33
|
+
.description('Create a sub-issue linked to a parent issue')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
try {
|
|
36
|
+
await (0, create_sub_1.createSubIssue)();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error('❌ Error:', error instanceof Error ? error.message : error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
program.parse();
|