brn-toolkit 1.0.0 → 1.0.2
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/dist/lib/utils.js +12 -8
- package/dist/skills/github/scripts/get_issue.js +46 -0
- package/dist/skills/github/scripts/get_repo_info.js +22 -16
- package/dist/skills/github/scripts/list_prs.js +26 -20
- package/dist/skills/jira/scripts/get_ticket.js +32 -26
- package/dist/skills/workflow/scripts/start.js +103 -29
- package/dist/skills/workspace-manager/scripts/create_workspace.js +5 -3
- package/lib/utils.ts +10 -4
- package/package.json +2 -1
- package/skills/git-worktree/SKILL.md +1 -1
- package/skills/github/SKILL.md +1 -1
- package/skills/github/scripts/get_issue.ts +66 -0
- package/skills/github/scripts/get_repo_info.ts +24 -17
- package/skills/github/scripts/list_prs.ts +29 -22
- package/skills/jira/SKILL.md +1 -1
- package/skills/jira/scripts/get_ticket.ts +33 -26
- package/skills/workflow/SKILL.md +2 -2
- package/skills/workflow/scripts/start.ts +117 -30
- package/skills/workspace-manager/SKILL.md +1 -1
- package/skills/workspace-manager/scripts/create_workspace.ts +5 -3
package/dist/lib/utils.js
CHANGED
|
@@ -118,15 +118,19 @@ export function getJiraConfig() {
|
|
|
118
118
|
export async function githubRequest(endpoint, options = {}) {
|
|
119
119
|
const token = getGitHubToken();
|
|
120
120
|
console.log(`[GitHub API] Requesting: ${endpoint}`);
|
|
121
|
-
const
|
|
121
|
+
const url = `https://api.github.com${endpoint}`;
|
|
122
|
+
const headers = {
|
|
123
|
+
Authorization: `Bearer ${token}`,
|
|
124
|
+
Accept: "application/vnd.github+json",
|
|
125
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
...options.headers,
|
|
128
|
+
};
|
|
129
|
+
console.log(` URL: ${url}`);
|
|
130
|
+
console.log(` Headers: ${JSON.stringify(headers, null, 2)}`);
|
|
131
|
+
const response = await fetch(url, {
|
|
122
132
|
...options,
|
|
123
|
-
headers
|
|
124
|
-
Authorization: `Bearer ${token}`,
|
|
125
|
-
Accept: "application/vnd.github+json",
|
|
126
|
-
"X-GitHub-Api-Version": "2022-11-28",
|
|
127
|
-
"Content-Type": "application/json",
|
|
128
|
-
...options.headers,
|
|
129
|
-
},
|
|
133
|
+
headers,
|
|
130
134
|
});
|
|
131
135
|
if (!response.ok) {
|
|
132
136
|
const error = await response.json().catch(() => ({}));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Get GitHub issue details
|
|
4
|
+
* Usage: npx tsx get_issue.ts <owner/repo/issues/number> or <owner/repo> <issue_number>
|
|
5
|
+
*/
|
|
6
|
+
import { githubRequest } from "../../../lib/utils.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
let repoPath = process.argv[2];
|
|
9
|
+
let issueNumber = process.argv[3];
|
|
10
|
+
// Handle URL-like format: owner/repo/issues/123
|
|
11
|
+
if (repoPath && repoPath.includes("/issues/")) {
|
|
12
|
+
const parts = repoPath.split("/");
|
|
13
|
+
issueNumber = parts.pop();
|
|
14
|
+
parts.pop(); // remove 'issues'
|
|
15
|
+
repoPath = parts.join("/");
|
|
16
|
+
}
|
|
17
|
+
if (!repoPath || !issueNumber) {
|
|
18
|
+
console.log("Usage: npx tsx get_issue.ts <owner/repo> <issue_number>");
|
|
19
|
+
console.log("Example: npx tsx get_issue.ts octocat/Hello-World 1");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const issue = await githubRequest(`/repos/${repoPath}/issues/${issueNumber}`);
|
|
23
|
+
console.log(`Issue: #${issue.number} ${issue.title}`);
|
|
24
|
+
console.log("=".repeat(60));
|
|
25
|
+
console.log(`Status: ${issue.state.toUpperCase()}`);
|
|
26
|
+
console.log(`Author: @${issue.user.login}`);
|
|
27
|
+
console.log(`Assignee: ${issue.assignee ? "@" + issue.assignee.login : "Unassigned"}`);
|
|
28
|
+
console.log(`Labels: ${issue.labels.map(l => l.name).join(", ") || "None"}`);
|
|
29
|
+
console.log(`Created: ${issue.created_at.slice(0, 10)}`);
|
|
30
|
+
console.log(`Updated: ${issue.updated_at.slice(0, 10)}`);
|
|
31
|
+
console.log();
|
|
32
|
+
if (issue.body) {
|
|
33
|
+
console.log("Description:");
|
|
34
|
+
console.log("-".repeat(40));
|
|
35
|
+
console.log(issue.body);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log("No description provided.");
|
|
39
|
+
}
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(`URL: ${issue.html_url}`);
|
|
42
|
+
}
|
|
43
|
+
main().catch(err => {
|
|
44
|
+
console.error("❌ Error:", err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
@@ -4,20 +4,26 @@
|
|
|
4
4
|
* Usage: npx tsx get_repo_info.ts <owner/repo>
|
|
5
5
|
*/
|
|
6
6
|
import { githubRequest } from "../../../lib/utils.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
async function main() {
|
|
8
|
+
const repoPath = process.argv[2];
|
|
9
|
+
if (!repoPath) {
|
|
10
|
+
console.log("Usage: npx tsx get_repo_info.ts <owner/repo>");
|
|
11
|
+
console.log("Example: npx tsx get_repo_info.ts octocat/Hello-World");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const repo = await githubRequest(`/repos/${repoPath}`);
|
|
15
|
+
console.log(`Repository: ${repo.full_name}`);
|
|
16
|
+
console.log("=".repeat(50));
|
|
17
|
+
console.log(`Description: ${repo.description ?? "N/A"}`);
|
|
18
|
+
console.log(`Visibility: ${repo.private ? "Private" : "Public"}`);
|
|
19
|
+
console.log(`Default: ${repo.default_branch}`);
|
|
20
|
+
console.log(`Language: ${repo.language ?? "N/A"}`);
|
|
21
|
+
console.log(`Stars: ${repo.stargazers_count}`);
|
|
22
|
+
console.log(`Forks: ${repo.forks_count}`);
|
|
23
|
+
console.log(`Clone URL: ${repo.clone_url}`);
|
|
24
|
+
console.log(`SSH URL: ${repo.ssh_url}`);
|
|
12
25
|
}
|
|
13
|
-
|
|
14
|
-
console.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.log(`Visibility: ${repo.private ? "Private" : "Public"}`);
|
|
18
|
-
console.log(`Default: ${repo.default_branch}`);
|
|
19
|
-
console.log(`Language: ${repo.language ?? "N/A"}`);
|
|
20
|
-
console.log(`Stars: ${repo.stargazers_count}`);
|
|
21
|
-
console.log(`Forks: ${repo.forks_count}`);
|
|
22
|
-
console.log(`Clone URL: ${repo.clone_url}`);
|
|
23
|
-
console.log(`SSH URL: ${repo.ssh_url}`);
|
|
26
|
+
main().catch(err => {
|
|
27
|
+
console.error("❌ Error:", err.message);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
@@ -4,24 +4,30 @@
|
|
|
4
4
|
* Usage: npx tsx list_prs.ts <owner/repo> [state]
|
|
5
5
|
*/
|
|
6
6
|
import { githubRequest } from "../../../lib/utils.js";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
console.log(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
7
|
+
async function main() {
|
|
8
|
+
const repoPath = process.argv[2];
|
|
9
|
+
const state = process.argv[3] ?? "open";
|
|
10
|
+
if (!repoPath) {
|
|
11
|
+
console.log("Usage: npx tsx list_prs.ts <owner/repo> [state]");
|
|
12
|
+
console.log("States: open, closed, all (default: open)");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const prs = await githubRequest(`/repos/${repoPath}/pulls?state=${state}&per_page=30`);
|
|
16
|
+
console.log(`Pull Requests for ${repoPath} (${state}):`);
|
|
17
|
+
console.log("=".repeat(50));
|
|
18
|
+
if (prs.length === 0) {
|
|
19
|
+
console.log("No pull requests found.");
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
for (const pr of prs) {
|
|
23
|
+
const draft = pr.draft ? "📝" : "";
|
|
24
|
+
console.log(`#${pr.number} ${draft} ${pr.title}`);
|
|
25
|
+
console.log(` ${pr.head.ref} → ${pr.base.ref} by @${pr.user.login}`);
|
|
26
|
+
console.log(` ${pr.html_url}`);
|
|
27
|
+
console.log();
|
|
28
|
+
}
|
|
27
29
|
}
|
|
30
|
+
main().catch(err => {
|
|
31
|
+
console.error("❌ Error:", err.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -16,30 +16,36 @@ function extractText(node) {
|
|
|
16
16
|
}
|
|
17
17
|
return "";
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
console.log(
|
|
30
|
-
console.log(
|
|
31
|
-
console.log(`
|
|
32
|
-
console.log(`
|
|
33
|
-
console.log(`
|
|
34
|
-
console.log(`
|
|
35
|
-
console.log(`
|
|
36
|
-
console.log(`
|
|
37
|
-
console.log(`
|
|
38
|
-
console.log();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
async function main() {
|
|
20
|
+
const ticketKey = process.argv[2];
|
|
21
|
+
if (!ticketKey) {
|
|
22
|
+
console.log("Usage: npx tsx get_ticket.ts <ticket_key>");
|
|
23
|
+
console.log("Example: npx tsx get_ticket.ts PROJ-123");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const { url } = getJiraConfig();
|
|
27
|
+
const issue = await jiraRequest(`/rest/api/3/issue/${ticketKey}`);
|
|
28
|
+
const { fields } = issue;
|
|
29
|
+
console.log(`Ticket: ${issue.key}`);
|
|
30
|
+
console.log("=".repeat(60));
|
|
31
|
+
console.log(`Summary: ${fields.summary}`);
|
|
32
|
+
console.log(`Status: ${fields.status.name}`);
|
|
33
|
+
console.log(`Type: ${fields.issuetype.name}`);
|
|
34
|
+
console.log(`Priority: ${fields.priority?.name ?? "None"}`);
|
|
35
|
+
console.log(`Assignee: ${fields.assignee?.displayName ?? "Unassigned"}`);
|
|
36
|
+
console.log(`Reporter: ${fields.reporter?.displayName ?? "Unknown"}`);
|
|
37
|
+
console.log(`Created: ${fields.created.slice(0, 10)}`);
|
|
38
|
+
console.log(`Updated: ${fields.updated.slice(0, 10)}`);
|
|
39
|
+
console.log();
|
|
40
|
+
if (fields.description) {
|
|
41
|
+
console.log("Description:");
|
|
42
|
+
console.log("-".repeat(40));
|
|
43
|
+
console.log(extractText(fields.description));
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(`URL: ${url}/browse/${issue.key}`);
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
console.
|
|
48
|
+
main().catch(err => {
|
|
49
|
+
console.error("❌ Error:", err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
@@ -1,38 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
/**
|
|
3
|
-
* Initiate a new development workflow for a JIRA ticket
|
|
4
|
-
* Usage: npx tsx start.ts <
|
|
3
|
+
* Initiate a new development workflow for a task (JIRA ticket or GitHub issue)
|
|
4
|
+
* Usage: npx tsx start.ts <task_id_or_url>
|
|
5
5
|
*/
|
|
6
6
|
import { $ } from "zx";
|
|
7
|
-
import { getActiveWorkspace, jiraRequest } from "../../../lib/utils.js";
|
|
7
|
+
import { getActiveWorkspace, jiraRequest, githubRequest, checkAutomation } from "../../../lib/utils.js";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
$.verbose = false;
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
13
|
-
console.log("Usage: npx tsx start.ts <
|
|
14
|
-
console.log("Example: npx tsx start.ts PROJ-123");
|
|
11
|
+
const input = process.argv[2];
|
|
12
|
+
if (!input) {
|
|
13
|
+
console.log("Usage: npx tsx start.ts <task_id_or_url>");
|
|
14
|
+
console.log("Example (JIRA): npx tsx start.ts PROJ-123");
|
|
15
|
+
console.log("Example (GitHub): npx tsx start.ts https://github.com/owner/repo/issues/42");
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
|
-
async function
|
|
18
|
-
|
|
19
|
-
// 1.
|
|
20
|
-
|
|
18
|
+
async function fetchTaskDetails(input) {
|
|
19
|
+
const workspace = getActiveWorkspace();
|
|
20
|
+
// 1. GitHub URL: https://github.com/owner/repo/issues/123
|
|
21
|
+
const githubUrlMatch = input.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
22
|
+
if (githubUrlMatch) {
|
|
23
|
+
const [, owner, repo, issueNumber] = githubUrlMatch;
|
|
24
|
+
const repoPath = `${owner}/${repo}`;
|
|
25
|
+
console.log(`📋 Fetching GitHub issue ${issueNumber} from ${repoPath}...`);
|
|
26
|
+
const issue = await githubRequest(`/repos/${repoPath}/issues/${issueNumber}`);
|
|
27
|
+
return {
|
|
28
|
+
id: issueNumber,
|
|
29
|
+
summary: issue.title,
|
|
30
|
+
description: issue.body ?? "",
|
|
31
|
+
source: "github",
|
|
32
|
+
url: issue.html_url,
|
|
33
|
+
repoPath: repoPath,
|
|
34
|
+
repoName: repo
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// 2. JIRA URL: https://org.atlassian.net/browse/PROJ-123
|
|
38
|
+
const jiraUrlMatch = input.match(/https:\/\/([^/]+)\.atlassian\.net\/browse\/([A-Z]+-\d+)/);
|
|
39
|
+
if (jiraUrlMatch) {
|
|
40
|
+
const [, , ticketId] = jiraUrlMatch;
|
|
41
|
+
return fetchJiraTicket(ticketId);
|
|
42
|
+
}
|
|
43
|
+
// 3. JIRA ID: PROJ-123
|
|
44
|
+
const jiraIdMatch = input.match(/^[A-Z]+-\d+$/);
|
|
45
|
+
if (jiraIdMatch) {
|
|
46
|
+
return fetchJiraTicket(input);
|
|
47
|
+
}
|
|
48
|
+
// 4. Fallback to default ticketing system if it's just a number
|
|
49
|
+
if (/^\d+$/.test(input) && workspace.default_ticketing_system === "github") {
|
|
50
|
+
console.error("❌ Error: For GitHub issues, please provide the full URL or owner/repo context.");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// If it looks like a JIRA ID but didn't match (e.g. lowercase), try anyway if default is jira
|
|
54
|
+
if (workspace.default_ticketing_system === "jira") {
|
|
55
|
+
return fetchJiraTicket(input);
|
|
56
|
+
}
|
|
57
|
+
console.error("❌ Error: Could not identify task source. Please provide a valid JIRA ID or GitHub Issue URL.");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
async function fetchJiraTicket(ticketId) {
|
|
61
|
+
const { jira_url } = getActiveWorkspace();
|
|
62
|
+
console.log(`📋 Fetching JIRA ticket ${ticketId}...`);
|
|
21
63
|
const ticket = await jiraRequest(`/rest/api/3/issue/${ticketId}`);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
function extractText(node) {
|
|
65
|
+
if (!node)
|
|
66
|
+
return "";
|
|
67
|
+
if (typeof node === "string")
|
|
68
|
+
return node;
|
|
69
|
+
if (node.type === "text")
|
|
70
|
+
return node.text;
|
|
71
|
+
if (Array.isArray(node.content))
|
|
72
|
+
return node.content.map(extractText).join(" ");
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
id: ticketId,
|
|
77
|
+
summary: ticket.fields.summary,
|
|
78
|
+
description: extractText(ticket.fields.description),
|
|
79
|
+
source: "jira",
|
|
80
|
+
url: `${jira_url}/browse/${ticketId}`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function run() {
|
|
84
|
+
const task = await fetchTaskDetails(input);
|
|
85
|
+
console.log(`✅ Task found: ${task.summary}`);
|
|
27
86
|
const workspace = getActiveWorkspace();
|
|
28
87
|
let workDir = workspace.path;
|
|
29
88
|
if (workDir.startsWith("~")) {
|
|
30
89
|
workDir = join(homedir(), workDir.slice(1));
|
|
31
90
|
}
|
|
32
|
-
// In a real scenario, we might have a mapping or ask the user
|
|
33
|
-
// For this automation, we'll try to find a repo that matches the project key or ask
|
|
34
91
|
console.log(`📂 Searching for repositories in ${workDir}...`);
|
|
35
|
-
// List directories in workDir
|
|
36
92
|
const repos = (await $ `ls -d ${workDir}/*/ 2>/dev/null`.quiet()).stdout
|
|
37
93
|
.split("\n")
|
|
38
94
|
.map(p => p.trim())
|
|
@@ -42,27 +98,45 @@ async function run() {
|
|
|
42
98
|
console.error("❌ No repositories found in workspace. Please clone a repo first using git-worktree:clone.");
|
|
43
99
|
process.exit(1);
|
|
44
100
|
}
|
|
101
|
+
// Try to match repoName if available
|
|
45
102
|
let selectedRepo = repos[0];
|
|
46
|
-
if (
|
|
103
|
+
if (task.repoName && repos.includes(task.repoName)) {
|
|
104
|
+
selectedRepo = task.repoName;
|
|
105
|
+
console.log(`🎯 Matched repository: ${selectedRepo}`);
|
|
106
|
+
}
|
|
107
|
+
else if (repos.length > 1) {
|
|
47
108
|
console.log("Multiple repositories found:");
|
|
48
109
|
repos.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
|
|
49
|
-
console.log(`Using ${selectedRepo} (default)
|
|
110
|
+
console.log(`Using ${selectedRepo} (default).`);
|
|
50
111
|
}
|
|
51
|
-
//
|
|
52
|
-
const branchName = `${
|
|
53
|
-
//
|
|
112
|
+
// Create branch name
|
|
113
|
+
const branchName = `${task.id}-${task.summary.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`;
|
|
114
|
+
// Create worktree
|
|
54
115
|
console.log(`🌿 Creating worktree for branch ${branchName}...`);
|
|
55
116
|
const createWorktreeScript = join(process.cwd(), "skills/git-worktree/scripts/create_worktree.sh");
|
|
56
117
|
await $ `${createWorktreeScript} ${selectedRepo} ${branchName}`;
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
118
|
+
// Update status/comment
|
|
119
|
+
if (task.source === "jira") {
|
|
120
|
+
console.log(`🔄 Updating JIRA status to 'In Progress'...`);
|
|
121
|
+
const updateTicketScript = join(process.cwd(), "skills/jira/scripts/update_ticket.ts");
|
|
122
|
+
await $ `npx tsx ${updateTicketScript} ${task.id} "In Progress"`.quiet();
|
|
123
|
+
}
|
|
124
|
+
else if (task.source === "github") {
|
|
125
|
+
if (checkAutomation("github_auto_comment", "Post a comment on the GitHub issue")) {
|
|
126
|
+
console.log(`💬 Adding comment to GitHub issue...`);
|
|
127
|
+
await githubRequest(`/repos/${task.repoPath}/issues/${task.id}/comments`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
body: `🚀 Started working on this issue. Branch: \`${branchName}\``
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
61
135
|
const worktreePath = join(workDir, `${selectedRepo}-worktrees`, branchName);
|
|
62
136
|
console.log("\n" + "=".repeat(50));
|
|
63
137
|
console.log(`✅ Workflow initiated successfully!`);
|
|
64
138
|
console.log(`📍 Worktree: ${worktreePath}`);
|
|
65
|
-
console.log(`🔗
|
|
139
|
+
console.log(`🔗 Task: ${task.url}`);
|
|
66
140
|
console.log("=".repeat(50));
|
|
67
141
|
console.log(`
|
|
68
142
|
Next steps:
|
|
@@ -10,9 +10,10 @@ import YAML from "yaml";
|
|
|
10
10
|
import { saveBrnConfig } from "../../../lib/utils.js";
|
|
11
11
|
const workspaceName = process.argv[2];
|
|
12
12
|
let workDir = process.argv[3];
|
|
13
|
+
const defaultTicketing = process.argv[4];
|
|
13
14
|
if (!workspaceName || !workDir) {
|
|
14
|
-
console.log("Usage: create_workspace.ts <name> <work_dir>");
|
|
15
|
-
console.log("Example: create_workspace.ts personal ~/dev/personal/auto");
|
|
15
|
+
console.log("Usage: create_workspace.ts <name> <work_dir> [default_ticketing]");
|
|
16
|
+
console.log("Example: create_workspace.ts personal ~/dev/personal/auto jira");
|
|
16
17
|
process.exit(1);
|
|
17
18
|
}
|
|
18
19
|
const configPath = join(homedir(), ".brn", "config.yaml");
|
|
@@ -46,7 +47,8 @@ config.workspaces[workspaceName] = {
|
|
|
46
47
|
path: workDir,
|
|
47
48
|
github_token: null,
|
|
48
49
|
jira_token: null,
|
|
49
|
-
jira_url: null
|
|
50
|
+
jira_url: null,
|
|
51
|
+
default_ticketing_system: defaultTicketing ?? "jira"
|
|
50
52
|
};
|
|
51
53
|
// If no active workspace, set this one
|
|
52
54
|
if (!config.active_workspace) {
|
package/lib/utils.ts
CHANGED
|
@@ -12,6 +12,7 @@ $.verbose = false;
|
|
|
12
12
|
export interface AutomationConfig {
|
|
13
13
|
github_auto_push?: boolean;
|
|
14
14
|
github_auto_pr?: boolean;
|
|
15
|
+
github_auto_comment?: boolean;
|
|
15
16
|
jira_auto_transition?: boolean;
|
|
16
17
|
jira_auto_comment?: boolean;
|
|
17
18
|
}
|
|
@@ -23,6 +24,7 @@ export interface WorkspaceConfig {
|
|
|
23
24
|
jira_token?: string | null;
|
|
24
25
|
jira_url?: string | null;
|
|
25
26
|
jira_email?: string | null;
|
|
27
|
+
default_ticketing_system?: "jira" | "github" | null;
|
|
26
28
|
automation?: AutomationConfig;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -179,15 +181,19 @@ export async function githubRequest<T>(
|
|
|
179
181
|
|
|
180
182
|
console.log(`[GitHub API] Requesting: ${endpoint}`);
|
|
181
183
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
headers: {
|
|
184
|
+
const url = `https://api.github.com${endpoint}`;
|
|
185
|
+
const headers = {
|
|
185
186
|
Authorization: `Bearer ${token}`,
|
|
186
187
|
Accept: "application/vnd.github+json",
|
|
187
188
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
188
189
|
"Content-Type": "application/json",
|
|
189
190
|
...options.headers,
|
|
190
|
-
}
|
|
191
|
+
};
|
|
192
|
+
console.log(` URL: ${url}`);
|
|
193
|
+
console.log(` Headers: ${JSON.stringify(headers, null, 2)}`);
|
|
194
|
+
const response = await fetch(url, {
|
|
195
|
+
...options,
|
|
196
|
+
headers,
|
|
191
197
|
});
|
|
192
198
|
|
|
193
199
|
if (!response.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brn-toolkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AI Developer Workflow Toolkit - Skills for Claude Code and Copilot CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "zfael",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"setup": "npx tsx cli/brn.ts setup",
|
|
28
28
|
"github:repos": "npx tsx skills/github/scripts/list_repos.ts",
|
|
29
29
|
"github:repo": "npx tsx skills/github/scripts/get_repo_info.ts",
|
|
30
|
+
"github:issue": "npx tsx skills/github/scripts/get_issue.ts",
|
|
30
31
|
"github:pr:create": "npx tsx skills/github/scripts/create_pr.ts",
|
|
31
32
|
"github:pr:list": "npx tsx skills/github/scripts/list_prs.ts",
|
|
32
33
|
"jira:tickets": "npx tsx skills/jira/scripts/list_tickets.ts",
|
package/skills/github/SKILL.md
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Get GitHub issue details
|
|
4
|
+
* Usage: npx tsx get_issue.ts <owner/repo/issues/number> or <owner/repo> <issue_number>
|
|
5
|
+
*/
|
|
6
|
+
import { githubRequest } from "../../../lib/utils.js";
|
|
7
|
+
|
|
8
|
+
interface Issue {
|
|
9
|
+
number: number;
|
|
10
|
+
title: string;
|
|
11
|
+
state: string;
|
|
12
|
+
body: string | null;
|
|
13
|
+
user: { login: string };
|
|
14
|
+
assignee: { login: string } | null;
|
|
15
|
+
labels: { name: string }[];
|
|
16
|
+
created_at: string;
|
|
17
|
+
updated_at: string;
|
|
18
|
+
html_url: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
let repoPath = process.argv[2];
|
|
23
|
+
let issueNumber = process.argv[3];
|
|
24
|
+
|
|
25
|
+
// Handle URL-like format: owner/repo/issues/123
|
|
26
|
+
if (repoPath && repoPath.includes("/issues/")) {
|
|
27
|
+
const parts = repoPath.split("/");
|
|
28
|
+
issueNumber = parts.pop()!;
|
|
29
|
+
parts.pop(); // remove 'issues'
|
|
30
|
+
repoPath = parts.join("/");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!repoPath || !issueNumber) {
|
|
34
|
+
console.log("Usage: npx tsx get_issue.ts <owner/repo> <issue_number>");
|
|
35
|
+
console.log("Example: npx tsx get_issue.ts octocat/Hello-World 1");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const issue = await githubRequest<Issue>(`/repos/${repoPath}/issues/${issueNumber}`);
|
|
40
|
+
|
|
41
|
+
console.log(`Issue: #${issue.number} ${issue.title}`);
|
|
42
|
+
console.log("=".repeat(60));
|
|
43
|
+
console.log(`Status: ${issue.state.toUpperCase()}`);
|
|
44
|
+
console.log(`Author: @${issue.user.login}`);
|
|
45
|
+
console.log(`Assignee: ${issue.assignee ? "@" + issue.assignee.login : "Unassigned"}`);
|
|
46
|
+
console.log(`Labels: ${issue.labels.map(l => l.name).join(", ") || "None"}`);
|
|
47
|
+
console.log(`Created: ${issue.created_at.slice(0, 10)}`);
|
|
48
|
+
console.log(`Updated: ${issue.updated_at.slice(0, 10)}`);
|
|
49
|
+
console.log();
|
|
50
|
+
|
|
51
|
+
if (issue.body) {
|
|
52
|
+
console.log("Description:");
|
|
53
|
+
console.log("-".repeat(40));
|
|
54
|
+
console.log(issue.body);
|
|
55
|
+
} else {
|
|
56
|
+
console.log("No description provided.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(`URL: ${issue.html_url}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main().catch(err => {
|
|
64
|
+
console.error("❌ Error:", err.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
@@ -17,23 +17,30 @@ interface Repo {
|
|
|
17
17
|
ssh_url: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
async function main() {
|
|
21
|
+
const repoPath = process.argv[2];
|
|
21
22
|
|
|
22
|
-
if (!repoPath) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
23
|
+
if (!repoPath) {
|
|
24
|
+
console.log("Usage: npx tsx get_repo_info.ts <owner/repo>");
|
|
25
|
+
console.log("Example: npx tsx get_repo_info.ts octocat/Hello-World");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
27
28
|
|
|
28
|
-
const repo = await githubRequest<Repo>(`/repos/${repoPath}`);
|
|
29
|
+
const repo = await githubRequest<Repo>(`/repos/${repoPath}`);
|
|
29
30
|
|
|
30
|
-
console.log(`Repository: ${repo.full_name}`);
|
|
31
|
-
console.log("=".repeat(50));
|
|
32
|
-
console.log(`Description: ${repo.description ?? "N/A"}`);
|
|
33
|
-
console.log(`Visibility: ${repo.private ? "Private" : "Public"}`);
|
|
34
|
-
console.log(`Default: ${repo.default_branch}`);
|
|
35
|
-
console.log(`Language: ${repo.language ?? "N/A"}`);
|
|
36
|
-
console.log(`Stars: ${repo.stargazers_count}`);
|
|
37
|
-
console.log(`Forks: ${repo.forks_count}`);
|
|
38
|
-
console.log(`Clone URL: ${repo.clone_url}`);
|
|
39
|
-
console.log(`SSH URL: ${repo.ssh_url}`);
|
|
31
|
+
console.log(`Repository: ${repo.full_name}`);
|
|
32
|
+
console.log("=".repeat(50));
|
|
33
|
+
console.log(`Description: ${repo.description ?? "N/A"}`);
|
|
34
|
+
console.log(`Visibility: ${repo.private ? "Private" : "Public"}`);
|
|
35
|
+
console.log(`Default: ${repo.default_branch}`);
|
|
36
|
+
console.log(`Language: ${repo.language ?? "N/A"}`);
|
|
37
|
+
console.log(`Stars: ${repo.stargazers_count}`);
|
|
38
|
+
console.log(`Forks: ${repo.forks_count}`);
|
|
39
|
+
console.log(`Clone URL: ${repo.clone_url}`);
|
|
40
|
+
console.log(`SSH URL: ${repo.ssh_url}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch(err => {
|
|
44
|
+
console.error("❌ Error:", err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
@@ -15,31 +15,38 @@ interface PR {
|
|
|
15
15
|
user: { login: string };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const
|
|
18
|
+
async function main() {
|
|
19
|
+
const repoPath = process.argv[2];
|
|
20
|
+
const state = process.argv[3] ?? "open";
|
|
20
21
|
|
|
21
|
-
if (!repoPath) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
22
|
+
if (!repoPath) {
|
|
23
|
+
console.log("Usage: npx tsx list_prs.ts <owner/repo> [state]");
|
|
24
|
+
console.log("States: open, closed, all (default: open)");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
26
27
|
|
|
27
|
-
const prs = await githubRequest<PR[]>(
|
|
28
|
-
|
|
29
|
-
);
|
|
28
|
+
const prs = await githubRequest<PR[]>(
|
|
29
|
+
`/repos/${repoPath}/pulls?state=${state}&per_page=30`
|
|
30
|
+
);
|
|
30
31
|
|
|
31
|
-
console.log(`Pull Requests for ${repoPath} (${state}):`);
|
|
32
|
-
console.log("=".repeat(50));
|
|
32
|
+
console.log(`Pull Requests for ${repoPath} (${state}):`);
|
|
33
|
+
console.log("=".repeat(50));
|
|
33
34
|
|
|
34
|
-
if (prs.length === 0) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
35
|
+
if (prs.length === 0) {
|
|
36
|
+
console.log("No pull requests found.");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
38
39
|
|
|
39
|
-
for (const pr of prs) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
for (const pr of prs) {
|
|
41
|
+
const draft = pr.draft ? "📝" : "";
|
|
42
|
+
console.log(`#${pr.number} ${draft} ${pr.title}`);
|
|
43
|
+
console.log(` ${pr.head.ref} → ${pr.base.ref} by @${pr.user.login}`);
|
|
44
|
+
console.log(` ${pr.html_url}`);
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
main().catch(err => {
|
|
50
|
+
console.error("❌ Error:", err.message);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
package/skills/jira/SKILL.md
CHANGED
|
@@ -36,36 +36,43 @@ function extractText(node: unknown): string {
|
|
|
36
36
|
return "";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
async function main() {
|
|
40
|
+
const ticketKey = process.argv[2];
|
|
40
41
|
|
|
41
|
-
if (!ticketKey) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
42
|
+
if (!ticketKey) {
|
|
43
|
+
console.log("Usage: npx tsx get_ticket.ts <ticket_key>");
|
|
44
|
+
console.log("Example: npx tsx get_ticket.ts PROJ-123");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
46
47
|
|
|
47
|
-
const { url } = getJiraConfig();
|
|
48
|
-
const issue = await jiraRequest<Issue>(`/rest/api/3/issue/${ticketKey}`);
|
|
48
|
+
const { url } = getJiraConfig();
|
|
49
|
+
const issue = await jiraRequest<Issue>(`/rest/api/3/issue/${ticketKey}`);
|
|
49
50
|
|
|
50
|
-
const { fields } = issue;
|
|
51
|
+
const { fields } = issue;
|
|
51
52
|
|
|
52
|
-
console.log(`Ticket: ${issue.key}`);
|
|
53
|
-
console.log("=".repeat(60));
|
|
54
|
-
console.log(`Summary: ${fields.summary}`);
|
|
55
|
-
console.log(`Status: ${fields.status.name}`);
|
|
56
|
-
console.log(`Type: ${fields.issuetype.name}`);
|
|
57
|
-
console.log(`Priority: ${fields.priority?.name ?? "None"}`);
|
|
58
|
-
console.log(`Assignee: ${fields.assignee?.displayName ?? "Unassigned"}`);
|
|
59
|
-
console.log(`Reporter: ${fields.reporter?.displayName ?? "Unknown"}`);
|
|
60
|
-
console.log(`Created: ${fields.created.slice(0, 10)}`);
|
|
61
|
-
console.log(`Updated: ${fields.updated.slice(0, 10)}`);
|
|
62
|
-
console.log();
|
|
53
|
+
console.log(`Ticket: ${issue.key}`);
|
|
54
|
+
console.log("=".repeat(60));
|
|
55
|
+
console.log(`Summary: ${fields.summary}`);
|
|
56
|
+
console.log(`Status: ${fields.status.name}`);
|
|
57
|
+
console.log(`Type: ${fields.issuetype.name}`);
|
|
58
|
+
console.log(`Priority: ${fields.priority?.name ?? "None"}`);
|
|
59
|
+
console.log(`Assignee: ${fields.assignee?.displayName ?? "Unassigned"}`);
|
|
60
|
+
console.log(`Reporter: ${fields.reporter?.displayName ?? "Unknown"}`);
|
|
61
|
+
console.log(`Created: ${fields.created.slice(0, 10)}`);
|
|
62
|
+
console.log(`Updated: ${fields.updated.slice(0, 10)}`);
|
|
63
|
+
console.log();
|
|
63
64
|
|
|
64
|
-
if (fields.description) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
if (fields.description) {
|
|
66
|
+
console.log("Description:");
|
|
67
|
+
console.log("-".repeat(40));
|
|
68
|
+
console.log(extractText(fields.description));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(`URL: ${url}/browse/${issue.key}`);
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
console.
|
|
75
|
+
main().catch(err => {
|
|
76
|
+
console.error("❌ Error:", err.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
package/skills/workflow/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: brn:workflow
|
|
2
|
+
name: brn-toolkit:workflow
|
|
3
3
|
description: |
|
|
4
4
|
Orchestrate the complete development workflow from ticket to PR.
|
|
5
5
|
Use when: (1) Starting work on a JIRA ticket, (2) Following the planning-coding-review cycle.
|
|
@@ -50,7 +50,7 @@ Initiates work on a specific ticket. This script:
|
|
|
50
50
|
## Detailed Phases
|
|
51
51
|
|
|
52
52
|
### Phase 1: Initiate
|
|
53
|
-
Start work on a JIRA ticket using the deterministic command:
|
|
53
|
+
Start work on a JIRA ticket or Github Issue using the deterministic command:
|
|
54
54
|
```bash
|
|
55
55
|
brn start PROJ-123
|
|
56
56
|
```
|
|
@@ -1,48 +1,119 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
/**
|
|
3
|
-
* Initiate a new development workflow for a JIRA ticket
|
|
4
|
-
* Usage: npx tsx start.ts <
|
|
3
|
+
* Initiate a new development workflow for a task (JIRA ticket or GitHub issue)
|
|
4
|
+
* Usage: npx tsx start.ts <task_id_or_url>
|
|
5
5
|
*/
|
|
6
6
|
import { $ } from "zx";
|
|
7
|
-
import { getActiveWorkspace, jiraRequest } from "../../../lib/utils.js";
|
|
7
|
+
import { getActiveWorkspace, jiraRequest, githubRequest, checkAutomation } from "../../../lib/utils.js";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { existsSync } from "fs";
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
|
|
12
12
|
$.verbose = false;
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const input = process.argv[2];
|
|
15
15
|
|
|
16
|
-
if (!
|
|
17
|
-
console.log("Usage: npx tsx start.ts <
|
|
18
|
-
console.log("Example: npx tsx start.ts PROJ-123");
|
|
16
|
+
if (!input) {
|
|
17
|
+
console.log("Usage: npx tsx start.ts <task_id_or_url>");
|
|
18
|
+
console.log("Example (JIRA): npx tsx start.ts PROJ-123");
|
|
19
|
+
console.log("Example (GitHub): npx tsx start.ts https://github.com/owner/repo/issues/42");
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
interface TaskDetails {
|
|
24
|
+
id: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
description: string;
|
|
27
|
+
source: "jira" | "github";
|
|
28
|
+
url: string;
|
|
29
|
+
repoPath?: string; // owner/repo
|
|
30
|
+
repoName?: string; // repo only
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function fetchTaskDetails(input: string): Promise<TaskDetails> {
|
|
34
|
+
const workspace = getActiveWorkspace();
|
|
35
|
+
|
|
36
|
+
// 1. GitHub URL: https://github.com/owner/repo/issues/123
|
|
37
|
+
const githubUrlMatch = input.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
|
|
38
|
+
if (githubUrlMatch) {
|
|
39
|
+
const [, owner, repo, issueNumber] = githubUrlMatch;
|
|
40
|
+
const repoPath = `${owner}/${repo}`;
|
|
41
|
+
console.log(`📋 Fetching GitHub issue ${issueNumber} from ${repoPath}...`);
|
|
42
|
+
|
|
43
|
+
const issue: any = await githubRequest(`/repos/${repoPath}/issues/${issueNumber}`);
|
|
44
|
+
return {
|
|
45
|
+
id: issueNumber,
|
|
46
|
+
summary: issue.title,
|
|
47
|
+
description: issue.body ?? "",
|
|
48
|
+
source: "github",
|
|
49
|
+
url: issue.html_url,
|
|
50
|
+
repoPath: repoPath,
|
|
51
|
+
repoName: repo
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. JIRA URL: https://org.atlassian.net/browse/PROJ-123
|
|
56
|
+
const jiraUrlMatch = input.match(/https:\/\/([^/]+)\.atlassian\.net\/browse\/([A-Z]+-\d+)/);
|
|
57
|
+
if (jiraUrlMatch) {
|
|
58
|
+
const [, , ticketId] = jiraUrlMatch;
|
|
59
|
+
return fetchJiraTicket(ticketId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. JIRA ID: PROJ-123
|
|
63
|
+
const jiraIdMatch = input.match(/^[A-Z]+-\d+$/);
|
|
64
|
+
if (jiraIdMatch) {
|
|
65
|
+
return fetchJiraTicket(input);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Fallback to default ticketing system if it's just a number
|
|
69
|
+
if (/^\d+$/.test(input) && workspace.default_ticketing_system === "github") {
|
|
70
|
+
console.error("❌ Error: For GitHub issues, please provide the full URL or owner/repo context.");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If it looks like a JIRA ID but didn't match (e.g. lowercase), try anyway if default is jira
|
|
75
|
+
if (workspace.default_ticketing_system === "jira") {
|
|
76
|
+
return fetchJiraTicket(input);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.error("❌ Error: Could not identify task source. Please provide a valid JIRA ID or GitHub Issue URL.");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
24
82
|
|
|
25
|
-
|
|
26
|
-
|
|
83
|
+
async function fetchJiraTicket(ticketId: string): Promise<TaskDetails> {
|
|
84
|
+
const { jira_url } = getActiveWorkspace();
|
|
85
|
+
console.log(`📋 Fetching JIRA ticket ${ticketId}...`);
|
|
27
86
|
const ticket: any = await jiraRequest(`/rest/api/3/issue/${ticketId}`);
|
|
28
|
-
const summary = ticket.fields.summary;
|
|
29
|
-
const projectKey = ticket.fields.project.key;
|
|
30
87
|
|
|
31
|
-
|
|
88
|
+
function extractText(node: any): string {
|
|
89
|
+
if (!node) return "";
|
|
90
|
+
if (typeof node === "string") return node;
|
|
91
|
+
if (node.type === "text") return node.text;
|
|
92
|
+
if (Array.isArray(node.content)) return node.content.map(extractText).join(" ");
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
id: ticketId,
|
|
98
|
+
summary: ticket.fields.summary,
|
|
99
|
+
description: extractText(ticket.fields.description),
|
|
100
|
+
source: "jira",
|
|
101
|
+
url: `${jira_url}/browse/${ticketId}`
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function run() {
|
|
106
|
+
const task = await fetchTaskDetails(input);
|
|
107
|
+
console.log(`✅ Task found: ${task.summary}`);
|
|
32
108
|
|
|
33
|
-
// 2. Identify repo (simple heuristic or prompt)
|
|
34
|
-
// For now, let's look for repos in the workspace
|
|
35
109
|
const workspace = getActiveWorkspace();
|
|
36
110
|
let workDir = workspace.path;
|
|
37
111
|
if (workDir.startsWith("~")) {
|
|
38
112
|
workDir = join(homedir(), workDir.slice(1));
|
|
39
113
|
}
|
|
40
114
|
|
|
41
|
-
// In a real scenario, we might have a mapping or ask the user
|
|
42
|
-
// For this automation, we'll try to find a repo that matches the project key or ask
|
|
43
115
|
console.log(`📂 Searching for repositories in ${workDir}...`);
|
|
44
116
|
|
|
45
|
-
// List directories in workDir
|
|
46
117
|
const repos = (await $`ls -d ${workDir}/*/ 2>/dev/null`.quiet()).stdout
|
|
47
118
|
.split("\n")
|
|
48
119
|
.map(p => p.trim())
|
|
@@ -54,32 +125,48 @@ async function run() {
|
|
|
54
125
|
process.exit(1);
|
|
55
126
|
}
|
|
56
127
|
|
|
128
|
+
// Try to match repoName if available
|
|
57
129
|
let selectedRepo = repos[0];
|
|
58
|
-
if (
|
|
130
|
+
if (task.repoName && repos.includes(task.repoName)) {
|
|
131
|
+
selectedRepo = task.repoName;
|
|
132
|
+
console.log(`🎯 Matched repository: ${selectedRepo}`);
|
|
133
|
+
} else if (repos.length > 1) {
|
|
59
134
|
console.log("Multiple repositories found:");
|
|
60
135
|
repos.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
|
|
61
|
-
console.log(`Using ${selectedRepo} (default)
|
|
136
|
+
console.log(`Using ${selectedRepo} (default).`);
|
|
62
137
|
}
|
|
63
138
|
|
|
64
|
-
//
|
|
65
|
-
const branchName = `${
|
|
139
|
+
// Create branch name
|
|
140
|
+
const branchName = `${task.id}-${task.summary.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`;
|
|
66
141
|
|
|
67
|
-
//
|
|
142
|
+
// Create worktree
|
|
68
143
|
console.log(`🌿 Creating worktree for branch ${branchName}...`);
|
|
69
144
|
const createWorktreeScript = join(process.cwd(), "skills/git-worktree/scripts/create_worktree.sh");
|
|
70
145
|
await $`${createWorktreeScript} ${selectedRepo} ${branchName}`;
|
|
71
146
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
147
|
+
// Update status/comment
|
|
148
|
+
if (task.source === "jira") {
|
|
149
|
+
console.log(`🔄 Updating JIRA status to 'In Progress'...`);
|
|
150
|
+
const updateTicketScript = join(process.cwd(), "skills/jira/scripts/update_ticket.ts");
|
|
151
|
+
await $`npx tsx ${updateTicketScript} ${task.id} "In Progress"`.quiet();
|
|
152
|
+
} else if (task.source === "github") {
|
|
153
|
+
if (checkAutomation("github_auto_comment", "Post a comment on the GitHub issue")) {
|
|
154
|
+
console.log(`💬 Adding comment to GitHub issue...`);
|
|
155
|
+
await githubRequest(`/repos/${task.repoPath}/issues/${task.id}/comments`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
body: `🚀 Started working on this issue. Branch: \`${branchName}\``
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
76
163
|
|
|
77
164
|
const worktreePath = join(workDir, `${selectedRepo}-worktrees`, branchName);
|
|
78
165
|
|
|
79
166
|
console.log("\n" + "=".repeat(50));
|
|
80
167
|
console.log(`✅ Workflow initiated successfully!`);
|
|
81
168
|
console.log(`📍 Worktree: ${worktreePath}`);
|
|
82
|
-
console.log(`🔗
|
|
169
|
+
console.log(`🔗 Task: ${task.url}`);
|
|
83
170
|
console.log("=".repeat(50));
|
|
84
171
|
console.log(`
|
|
85
172
|
Next steps:
|
|
@@ -90,4 +177,4 @@ Next steps:
|
|
|
90
177
|
run().catch(err => {
|
|
91
178
|
console.error("❌ Error initiating workflow:", err.message);
|
|
92
179
|
process.exit(1);
|
|
93
|
-
});
|
|
180
|
+
});
|
|
@@ -11,10 +11,11 @@ import { saveBrnConfig } from "../../../lib/utils.js";
|
|
|
11
11
|
|
|
12
12
|
const workspaceName = process.argv[2];
|
|
13
13
|
let workDir = process.argv[3];
|
|
14
|
+
const defaultTicketing = process.argv[4] as "jira" | "github" | undefined;
|
|
14
15
|
|
|
15
16
|
if (!workspaceName || !workDir) {
|
|
16
|
-
console.log("Usage: create_workspace.ts <name> <work_dir>");
|
|
17
|
-
console.log("Example: create_workspace.ts personal ~/dev/personal/auto");
|
|
17
|
+
console.log("Usage: create_workspace.ts <name> <work_dir> [default_ticketing]");
|
|
18
|
+
console.log("Example: create_workspace.ts personal ~/dev/personal/auto jira");
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -56,7 +57,8 @@ config.workspaces[workspaceName] = {
|
|
|
56
57
|
path: workDir,
|
|
57
58
|
github_token: null,
|
|
58
59
|
jira_token: null,
|
|
59
|
-
jira_url: null
|
|
60
|
+
jira_url: null,
|
|
61
|
+
default_ticketing_system: defaultTicketing ?? "jira"
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
// If no active workspace, set this one
|