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 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 response = await fetch(`https://api.github.com${endpoint}`, {
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
- const repoPath = process.argv[2];
8
- if (!repoPath) {
9
- console.log("Usage: npx tsx get_repo_info.ts <owner/repo>");
10
- console.log("Example: npx tsx get_repo_info.ts octocat/Hello-World");
11
- process.exit(1);
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
- const repo = await githubRequest(`/repos/${repoPath}`);
14
- console.log(`Repository: ${repo.full_name}`);
15
- console.log("=".repeat(50));
16
- console.log(`Description: ${repo.description ?? "N/A"}`);
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
- const repoPath = process.argv[2];
8
- const state = process.argv[3] ?? "open";
9
- if (!repoPath) {
10
- console.log("Usage: npx tsx list_prs.ts <owner/repo> [state]");
11
- console.log("States: open, closed, all (default: open)");
12
- process.exit(1);
13
- }
14
- const prs = await githubRequest(`/repos/${repoPath}/pulls?state=${state}&per_page=30`);
15
- console.log(`Pull Requests for ${repoPath} (${state}):`);
16
- console.log("=".repeat(50));
17
- if (prs.length === 0) {
18
- console.log("No pull requests found.");
19
- process.exit(0);
20
- }
21
- for (const pr of prs) {
22
- const draft = pr.draft ? "📝" : "";
23
- console.log(`#${pr.number} ${draft} ${pr.title}`);
24
- console.log(` ${pr.head.ref} ${pr.base.ref} by @${pr.user.login}`);
25
- console.log(` ${pr.html_url}`);
26
- console.log();
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
- const ticketKey = process.argv[2];
20
- if (!ticketKey) {
21
- console.log("Usage: npx tsx get_ticket.ts <ticket_key>");
22
- console.log("Example: npx tsx get_ticket.ts PROJ-123");
23
- process.exit(1);
24
- }
25
- const { url } = getJiraConfig();
26
- const issue = await jiraRequest(`/rest/api/3/issue/${ticketKey}`);
27
- const { fields } = issue;
28
- console.log(`Ticket: ${issue.key}`);
29
- console.log("=".repeat(60));
30
- console.log(`Summary: ${fields.summary}`);
31
- console.log(`Status: ${fields.status.name}`);
32
- console.log(`Type: ${fields.issuetype.name}`);
33
- console.log(`Priority: ${fields.priority?.name ?? "None"}`);
34
- console.log(`Assignee: ${fields.assignee?.displayName ?? "Unassigned"}`);
35
- console.log(`Reporter: ${fields.reporter?.displayName ?? "Unknown"}`);
36
- console.log(`Created: ${fields.created.slice(0, 10)}`);
37
- console.log(`Updated: ${fields.updated.slice(0, 10)}`);
38
- console.log();
39
- if (fields.description) {
40
- console.log("Description:");
41
- console.log("-".repeat(40));
42
- console.log(extractText(fields.description));
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
- console.log();
45
- console.log(`URL: ${url}/browse/${issue.key}`);
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 <ticket_id>
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 ticketId = process.argv[2];
12
- if (!ticketId) {
13
- console.log("Usage: npx tsx start.ts <ticket_id>");
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 run() {
18
- console.log(`🚀 Initiating workflow for ${ticketId}...`);
19
- // 1. Fetch ticket details
20
- console.log(`📋 Fetching ticket ${ticketId}...`);
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
- const summary = ticket.fields.summary;
23
- const projectKey = ticket.fields.project.key;
24
- console.log(`✅ Ticket found: ${summary}`);
25
- // 2. Identify repo (simple heuristic or prompt)
26
- // For now, let's look for repos in the workspace
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 (repos.length > 1) {
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). To use another, clone it or configure mapping.`);
110
+ console.log(`Using ${selectedRepo} (default).`);
50
111
  }
51
- // 3. Create branch name
52
- const branchName = `${ticketId}-${summary.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`;
53
- // 4. Create worktree
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
- // 5. Update ticket status
58
- console.log(`🔄 Updating ticket status to 'In Progress'...`);
59
- const updateTicketScript = join(process.cwd(), "skills/jira/scripts/update_ticket.ts");
60
- await $ `npx tsx ${updateTicketScript} ${ticketId} "In Progress"`.quiet();
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(`🔗 Ticket: ${workspace.jira_url}/browse/${ticketId}`);
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 response = await fetch(`https://api.github.com${endpoint}`, {
183
- ...options,
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.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",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: brn:git-worktree
2
+ name: brn-toolkit:git-worktree
3
3
  description: |
4
4
  Manage git repositories using the worktree pattern.
5
5
  This allows multiple branches to be checked out simultaneously in sibling directories.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: brn:github
2
+ name: brn-toolkit:github
3
3
  description: |
4
4
  Interact with GitHub API for repository and pull request management.
5
5
  Use when: (1) Listing repositories, (2) Getting repository info, (3) Creating PRs.
@@ -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
- const repoPath = process.argv[2];
20
+ async function main() {
21
+ const repoPath = process.argv[2];
21
22
 
22
- if (!repoPath) {
23
- console.log("Usage: npx tsx get_repo_info.ts <owner/repo>");
24
- console.log("Example: npx tsx get_repo_info.ts octocat/Hello-World");
25
- process.exit(1);
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
- const repoPath = process.argv[2];
19
- const state = process.argv[3] ?? "open";
18
+ async function main() {
19
+ const repoPath = process.argv[2];
20
+ const state = process.argv[3] ?? "open";
20
21
 
21
- if (!repoPath) {
22
- console.log("Usage: npx tsx list_prs.ts <owner/repo> [state]");
23
- console.log("States: open, closed, all (default: open)");
24
- process.exit(1);
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
- `/repos/${repoPath}/pulls?state=${state}&per_page=30`
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
- console.log("No pull requests found.");
36
- process.exit(0);
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
- const draft = pr.draft ? "📝" : "";
41
- console.log(`#${pr.number} ${draft} ${pr.title}`);
42
- console.log(` ${pr.head.ref} → ${pr.base.ref} by @${pr.user.login}`);
43
- console.log(` ${pr.html_url}`);
44
- console.log();
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
+ });
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: brn:jira
2
+ name: brn-toolkit:jira
3
3
  description: |
4
4
  Interact with JIRA API for ticket management and tracking.
5
5
  Use when: (1) Listing assigned tickets, (2) Getting details, (3) Updating status.
@@ -36,36 +36,43 @@ function extractText(node: unknown): string {
36
36
  return "";
37
37
  }
38
38
 
39
- const ticketKey = process.argv[2];
39
+ async function main() {
40
+ const ticketKey = process.argv[2];
40
41
 
41
- if (!ticketKey) {
42
- console.log("Usage: npx tsx get_ticket.ts <ticket_key>");
43
- console.log("Example: npx tsx get_ticket.ts PROJ-123");
44
- process.exit(1);
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
- console.log("Description:");
66
- console.log("-".repeat(40));
67
- console.log(extractText(fields.description));
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
- console.log();
71
- console.log(`URL: ${url}/browse/${issue.key}`);
75
+ main().catch(err => {
76
+ console.error("❌ Error:", err.message);
77
+ process.exit(1);
78
+ });
@@ -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 <ticket_id>
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 ticketId = process.argv[2];
14
+ const input = process.argv[2];
15
15
 
16
- if (!ticketId) {
17
- console.log("Usage: npx tsx start.ts <ticket_id>");
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
- async function run() {
23
- console.log(`🚀 Initiating workflow for ${ticketId}...`);
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
- // 1. Fetch ticket details
26
- console.log(`📋 Fetching ticket ${ticketId}...`);
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
- console.log(`✅ Ticket found: ${summary}`);
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 (repos.length > 1) {
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). To use another, clone it or configure mapping.`);
136
+ console.log(`Using ${selectedRepo} (default).`);
62
137
  }
63
138
 
64
- // 3. Create branch name
65
- const branchName = `${ticketId}-${summary.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`;
139
+ // Create branch name
140
+ const branchName = `${task.id}-${task.summary.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`;
66
141
 
67
- // 4. Create worktree
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
- // 5. Update ticket status
73
- console.log(`🔄 Updating ticket status to 'In Progress'...`);
74
- const updateTicketScript = join(process.cwd(), "skills/jira/scripts/update_ticket.ts");
75
- await $`npx tsx ${updateTicketScript} ${ticketId} "In Progress"`.quiet();
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(`🔗 Ticket: ${workspace.jira_url}/browse/${ticketId}`);
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
+ });
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: brn:workspace-manager
2
+ name: brn-toolkit:workspace-manager
3
3
  description: |
4
4
  Manage isolated configurations (tokens, paths) for different contexts.
5
5
  ---
@@ -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