brn-toolkit 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/GEMINI.md +92 -0
- package/README.md +145 -0
- package/cli/brn.ts +301 -0
- package/dist/cli/brn.js +274 -0
- package/dist/lib/utils.js +167 -0
- package/dist/skills/github/scripts/create_pr.js +24 -0
- package/dist/skills/github/scripts/get_repo_info.js +23 -0
- package/dist/skills/github/scripts/list_prs.js +27 -0
- package/dist/skills/github/scripts/list_repos.js +39 -0
- package/dist/skills/jira/scripts/add_comment.js +36 -0
- package/dist/skills/jira/scripts/get_ticket.js +45 -0
- package/dist/skills/jira/scripts/list_tickets.js +42 -0
- package/dist/skills/jira/scripts/update_ticket.js +30 -0
- package/dist/skills/workflow/scripts/start.js +75 -0
- package/dist/skills/workspace-manager/scripts/configure_workspace.js +59 -0
- package/dist/skills/workspace-manager/scripts/create_workspace.js +60 -0
- package/lib/utils.ts +236 -0
- package/package.json +46 -0
- package/skills/git-worktree/SKILL.md +49 -0
- package/skills/git-worktree/scripts/clone_repo.sh +53 -0
- package/skills/git-worktree/scripts/create_worktree.sh +66 -0
- package/skills/git-worktree/scripts/list_worktrees.sh +39 -0
- package/skills/git-worktree/scripts/remove_worktree.sh +47 -0
- package/skills/github/SKILL.md +51 -0
- package/skills/github/references/api_patterns.md +36 -0
- package/skills/github/scripts/create_pr.ts +38 -0
- package/skills/github/scripts/get_repo_info.ts +39 -0
- package/skills/github/scripts/list_prs.ts +45 -0
- package/skills/github/scripts/list_repos.ts +54 -0
- package/skills/jira/SKILL.md +50 -0
- package/skills/jira/references/api_patterns.md +59 -0
- package/skills/jira/scripts/add_comment.ts +41 -0
- package/skills/jira/scripts/get_ticket.ts +71 -0
- package/skills/jira/scripts/list_tickets.ts +64 -0
- package/skills/jira/scripts/update_ticket.ts +50 -0
- package/skills/workflow/SKILL.md +94 -0
- package/skills/workflow/references/coding_workflow.md +88 -0
- package/skills/workflow/references/planning_workflow.md +96 -0
- package/skills/workflow/references/review_workflow.md +104 -0
- package/skills/workflow/scripts/start.ts +93 -0
- package/skills/workspace-manager/SKILL.md +49 -0
- package/skills/workspace-manager/scripts/configure_workspace.sh +87 -0
- package/skills/workspace-manager/scripts/configure_workspace.ts +70 -0
- package/skills/workspace-manager/scripts/create_workspace.sh +66 -0
- package/skills/workspace-manager/scripts/create_workspace.ts +74 -0
- package/skills/workspace-manager/scripts/get_active_workspace.sh +24 -0
- package/skills/workspace-manager/scripts/list_workspaces.sh +31 -0
- package/skills/workspace-manager/scripts/switch_workspace.sh +38 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Update JIRA ticket status via transition
|
|
4
|
+
* Usage: npx tsx update_ticket.ts <ticket_key> <target_status>
|
|
5
|
+
*/
|
|
6
|
+
import { jiraRequest } from "../../../lib/utils.js";
|
|
7
|
+
const [ticketKey, targetStatus] = process.argv.slice(2);
|
|
8
|
+
if (!ticketKey || !targetStatus) {
|
|
9
|
+
console.log("Usage: npx tsx update_ticket.ts <ticket_key> <target_status>");
|
|
10
|
+
console.log("Example: npx tsx update_ticket.ts PROJ-123 'In Progress'");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
// Get available transitions
|
|
14
|
+
const { transitions } = await jiraRequest(`/rest/api/3/issue/${ticketKey}/transitions`);
|
|
15
|
+
// Find matching transition (case-insensitive)
|
|
16
|
+
const transition = transitions.find((t) => t.name.toLowerCase() === targetStatus.toLowerCase());
|
|
17
|
+
if (!transition) {
|
|
18
|
+
console.error(`Error: Transition '${targetStatus}' not available`);
|
|
19
|
+
console.error("Available transitions:");
|
|
20
|
+
for (const t of transitions) {
|
|
21
|
+
console.error(` - ${t.name}`);
|
|
22
|
+
}
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Perform transition
|
|
26
|
+
await jiraRequest(`/rest/api/3/issue/${ticketKey}/transitions`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: JSON.stringify({ transition: { id: transition.id } }),
|
|
29
|
+
});
|
|
30
|
+
console.log(`✓ Transitioned ${ticketKey} to '${targetStatus}'`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Initiate a new development workflow for a JIRA ticket
|
|
4
|
+
* Usage: npx tsx start.ts <ticket_id>
|
|
5
|
+
*/
|
|
6
|
+
import { $ } from "zx";
|
|
7
|
+
import { getActiveWorkspace, jiraRequest } from "../../../lib/utils.js";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
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");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
async function run() {
|
|
18
|
+
console.log(`🚀 Initiating workflow for ${ticketId}...`);
|
|
19
|
+
// 1. Fetch ticket details
|
|
20
|
+
console.log(`📋 Fetching ticket ${ticketId}...`);
|
|
21
|
+
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
|
|
27
|
+
const workspace = getActiveWorkspace();
|
|
28
|
+
let workDir = workspace.path;
|
|
29
|
+
if (workDir.startsWith("~")) {
|
|
30
|
+
workDir = join(homedir(), workDir.slice(1));
|
|
31
|
+
}
|
|
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
|
+
console.log(`📂 Searching for repositories in ${workDir}...`);
|
|
35
|
+
// List directories in workDir
|
|
36
|
+
const repos = (await $ `ls -d ${workDir}/*/ 2>/dev/null`.quiet()).stdout
|
|
37
|
+
.split("\n")
|
|
38
|
+
.map(p => p.trim())
|
|
39
|
+
.filter(p => p && !p.endsWith("-worktrees/"))
|
|
40
|
+
.map(p => p.split("/").filter(Boolean).pop());
|
|
41
|
+
if (repos.length === 0) {
|
|
42
|
+
console.error("❌ No repositories found in workspace. Please clone a repo first using git-worktree:clone.");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
let selectedRepo = repos[0];
|
|
46
|
+
if (repos.length > 1) {
|
|
47
|
+
console.log("Multiple repositories found:");
|
|
48
|
+
repos.forEach((r, i) => console.log(` ${i + 1}. ${r}`));
|
|
49
|
+
console.log(`Using ${selectedRepo} (default). To use another, clone it or configure mapping.`);
|
|
50
|
+
}
|
|
51
|
+
// 3. Create branch name
|
|
52
|
+
const branchName = `${ticketId}-${summary.toLowerCase().replace(/[^a-z0-9]/g, "-").slice(0, 30)}`;
|
|
53
|
+
// 4. Create worktree
|
|
54
|
+
console.log(`🌿 Creating worktree for branch ${branchName}...`);
|
|
55
|
+
const createWorktreeScript = join(process.cwd(), "skills/git-worktree/scripts/create_worktree.sh");
|
|
56
|
+
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();
|
|
61
|
+
const worktreePath = join(workDir, `${selectedRepo}-worktrees`, branchName);
|
|
62
|
+
console.log("\n" + "=".repeat(50));
|
|
63
|
+
console.log(`✅ Workflow initiated successfully!`);
|
|
64
|
+
console.log(`📍 Worktree: ${worktreePath}`);
|
|
65
|
+
console.log(`🔗 Ticket: ${workspace.jira_url}/browse/${ticketId}`);
|
|
66
|
+
console.log("=".repeat(50));
|
|
67
|
+
console.log(`
|
|
68
|
+
Next steps:
|
|
69
|
+
cd ${worktreePath}
|
|
70
|
+
# Start planning and coding!`);
|
|
71
|
+
}
|
|
72
|
+
run().catch(err => {
|
|
73
|
+
console.error("❌ Error initiating workflow:", err.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Configure a workspace setting
|
|
4
|
+
* Usage: npx tsx configure_workspace.ts <workspace_name> <key> <value>
|
|
5
|
+
*/
|
|
6
|
+
import { getBrnConfig, saveBrnConfig } from "../../../lib/utils.js";
|
|
7
|
+
const workspaceName = process.argv[2];
|
|
8
|
+
const key = process.argv[3];
|
|
9
|
+
const value = process.argv[4];
|
|
10
|
+
if (!workspaceName || !key || !value) {
|
|
11
|
+
console.log("Usage: configure_workspace.ts <workspace_name> <key> <value>");
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log("Standard keys: github_token, jira_token, jira_url, jira_email, path");
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log("Automation keys (default: false):");
|
|
16
|
+
console.log(" automation.github_auto_push - Auto-push commits");
|
|
17
|
+
console.log(" automation.github_auto_pr - Auto-create PRs");
|
|
18
|
+
console.log(" automation.jira_auto_transition - Auto-update ticket status");
|
|
19
|
+
console.log(" automation.jira_auto_comment - Auto-add comments");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const config = getBrnConfig();
|
|
23
|
+
if (!config.workspaces[workspaceName]) {
|
|
24
|
+
console.error(`Error: Workspace '${workspaceName}' not found`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
// Validate key
|
|
28
|
+
const validKeys = ["github_token", "github_org", "jira_token", "jira_url", "jira_email", "path"];
|
|
29
|
+
const validAutomationKeys = [
|
|
30
|
+
"automation.github_auto_push",
|
|
31
|
+
"automation.github_auto_pr",
|
|
32
|
+
"automation.jira_auto_transition",
|
|
33
|
+
"automation.jira_auto_comment"
|
|
34
|
+
];
|
|
35
|
+
let isValid = validKeys.includes(key) || validAutomationKeys.includes(key);
|
|
36
|
+
if (!isValid) {
|
|
37
|
+
console.error(`Error: Invalid key '${key}'`);
|
|
38
|
+
console.log(`Valid keys: ${validKeys.join(", ")}`);
|
|
39
|
+
console.log(`Automation keys: ${validAutomationKeys.join(", ")}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Handle automation keys (nested)
|
|
43
|
+
if (key.startsWith("automation.")) {
|
|
44
|
+
const automationKey = key.replace("automation.", "");
|
|
45
|
+
const boolValue = value === "true";
|
|
46
|
+
if (value !== "true" && value !== "false") {
|
|
47
|
+
console.error("Error: Automation values must be 'true' or 'false'");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (!config.workspaces[workspaceName].automation) {
|
|
51
|
+
config.workspaces[workspaceName].automation = {};
|
|
52
|
+
}
|
|
53
|
+
config.workspaces[workspaceName].automation[automationKey] = boolValue;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
config.workspaces[workspaceName][key] = value;
|
|
57
|
+
}
|
|
58
|
+
saveBrnConfig(config);
|
|
59
|
+
console.log(`✓ Set ${key} for workspace '${workspaceName}'`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Create a new workspace
|
|
4
|
+
* Usage: npx tsx create_workspace.ts <name> <work_dir>
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import YAML from "yaml";
|
|
10
|
+
import { saveBrnConfig } from "../../../lib/utils.js";
|
|
11
|
+
const workspaceName = process.argv[2];
|
|
12
|
+
let workDir = process.argv[3];
|
|
13
|
+
if (!workspaceName || !workDir) {
|
|
14
|
+
console.log("Usage: create_workspace.ts <name> <work_dir>");
|
|
15
|
+
console.log("Example: create_workspace.ts personal ~/dev/personal/auto");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const configPath = join(homedir(), ".brn", "config.yaml");
|
|
19
|
+
const configDir = join(homedir(), ".brn");
|
|
20
|
+
// Create config directory if it doesn't exist
|
|
21
|
+
if (!existsSync(configDir)) {
|
|
22
|
+
mkdirSync(configDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
// Expand work_dir path
|
|
25
|
+
if (workDir.startsWith("~")) {
|
|
26
|
+
workDir = join(homedir(), workDir.slice(1));
|
|
27
|
+
}
|
|
28
|
+
let config = {
|
|
29
|
+
version: "1.0",
|
|
30
|
+
active_workspace: workspaceName,
|
|
31
|
+
workspaces: {}
|
|
32
|
+
};
|
|
33
|
+
if (existsSync(configPath)) {
|
|
34
|
+
const content = readFileSync(configPath, "utf-8");
|
|
35
|
+
config = YAML.parse(content);
|
|
36
|
+
}
|
|
37
|
+
// Check if workspace already exists
|
|
38
|
+
if (config.workspaces && config.workspaces[workspaceName]) {
|
|
39
|
+
console.error(`Error: Workspace '${workspaceName}' already exists`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (!config.workspaces) {
|
|
43
|
+
config.workspaces = {};
|
|
44
|
+
}
|
|
45
|
+
config.workspaces[workspaceName] = {
|
|
46
|
+
path: workDir,
|
|
47
|
+
github_token: null,
|
|
48
|
+
jira_token: null,
|
|
49
|
+
jira_url: null
|
|
50
|
+
};
|
|
51
|
+
// If no active workspace, set this one
|
|
52
|
+
if (!config.active_workspace) {
|
|
53
|
+
config.active_workspace = workspaceName;
|
|
54
|
+
}
|
|
55
|
+
saveBrnConfig(config);
|
|
56
|
+
// Create the work directory
|
|
57
|
+
if (!existsSync(workDir)) {
|
|
58
|
+
mkdirSync(workDir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
console.log(`✓ Created workspace '${workspaceName}' at ${workDir}`);
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for brn scripts
|
|
3
|
+
*/
|
|
4
|
+
import { $ } from "zx";
|
|
5
|
+
import { readFileSync, existsSync, writeFileSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import YAML from "yaml";
|
|
9
|
+
|
|
10
|
+
$.verbose = false;
|
|
11
|
+
|
|
12
|
+
export interface AutomationConfig {
|
|
13
|
+
github_auto_push?: boolean;
|
|
14
|
+
github_auto_pr?: boolean;
|
|
15
|
+
jira_auto_transition?: boolean;
|
|
16
|
+
jira_auto_comment?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkspaceConfig {
|
|
20
|
+
path: string;
|
|
21
|
+
github_token?: string | null;
|
|
22
|
+
github_org?: string | null;
|
|
23
|
+
jira_token?: string | null;
|
|
24
|
+
jira_url?: string | null;
|
|
25
|
+
jira_email?: string | null;
|
|
26
|
+
automation?: AutomationConfig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BrnConfig {
|
|
30
|
+
version: string;
|
|
31
|
+
active_workspace: string;
|
|
32
|
+
workspaces: Record<string, WorkspaceConfig>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the brn config from ~/.brn/config.yaml
|
|
37
|
+
*/
|
|
38
|
+
export function getBrnConfig(): BrnConfig {
|
|
39
|
+
const configPath = join(homedir(), ".brn", "config.yaml");
|
|
40
|
+
|
|
41
|
+
if (!existsSync(configPath)) {
|
|
42
|
+
console.error("Error: No config found at ~/.brn/config.yaml");
|
|
43
|
+
console.error("Run: create_workspace.sh <name> <path>");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = readFileSync(configPath, "utf-8");
|
|
48
|
+
return YAML.parse(content) as BrnConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Save the brn config to ~/.brn/config.yaml
|
|
53
|
+
*/
|
|
54
|
+
export function saveBrnConfig(config: BrnConfig): void {
|
|
55
|
+
const configPath = join(homedir(), ".brn", "config.yaml");
|
|
56
|
+
writeFileSync(configPath, YAML.stringify(config));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update the active workspace configuration
|
|
61
|
+
*/
|
|
62
|
+
export function updateWorkspace(updates: Partial<WorkspaceConfig>): void {
|
|
63
|
+
const config = getBrnConfig();
|
|
64
|
+
const activeWorkspaceName = config.active_workspace;
|
|
65
|
+
|
|
66
|
+
if (!config.workspaces[activeWorkspaceName]) {
|
|
67
|
+
console.error(`Error: Active workspace '${activeWorkspaceName}' not found`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
config.workspaces[activeWorkspaceName] = {
|
|
72
|
+
...config.workspaces[activeWorkspaceName],
|
|
73
|
+
...updates,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
saveBrnConfig(config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the active workspace config
|
|
81
|
+
*/
|
|
82
|
+
export function getActiveWorkspace(): WorkspaceConfig & { name: string } {
|
|
83
|
+
const config = getBrnConfig();
|
|
84
|
+
const name = config.active_workspace;
|
|
85
|
+
const workspace = config.workspaces[name];
|
|
86
|
+
|
|
87
|
+
if (!workspace) {
|
|
88
|
+
console.error(`Error: Workspace '${name}' not found in config`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { name, ...workspace };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if an automation action is enabled for the active workspace.
|
|
97
|
+
* All automation is OFF by default for safety.
|
|
98
|
+
*/
|
|
99
|
+
export function isAutomationEnabled(
|
|
100
|
+
action: keyof AutomationConfig
|
|
101
|
+
): boolean {
|
|
102
|
+
const workspace = getActiveWorkspace();
|
|
103
|
+
return workspace.automation?.[action] ?? false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Require automation to be enabled, or prompt user to confirm.
|
|
108
|
+
* Returns true if action should proceed, false if user declined.
|
|
109
|
+
*/
|
|
110
|
+
export function checkAutomation(
|
|
111
|
+
action: keyof AutomationConfig,
|
|
112
|
+
description: string
|
|
113
|
+
): boolean {
|
|
114
|
+
if (isAutomationEnabled(action)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`⚠️ Automation disabled: ${description}`);
|
|
119
|
+
console.log(` To enable, run: configure_workspace.sh <workspace> automation.${action} true`);
|
|
120
|
+
console.log(` Skipping automatic execution.`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get GitHub token from active workspace
|
|
126
|
+
*/
|
|
127
|
+
export function getGitHubToken(): string {
|
|
128
|
+
const workspace = getActiveWorkspace();
|
|
129
|
+
|
|
130
|
+
if (!workspace.github_token) {
|
|
131
|
+
console.error("Error: github_token not configured for this workspace");
|
|
132
|
+
console.error(
|
|
133
|
+
`Run: configure_workspace.sh ${workspace.name} github_token <token>`
|
|
134
|
+
);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return workspace.github_token;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get JIRA config from active workspace
|
|
143
|
+
*/
|
|
144
|
+
export function getJiraConfig(): {
|
|
145
|
+
url: string;
|
|
146
|
+
email: string;
|
|
147
|
+
token: string;
|
|
148
|
+
} {
|
|
149
|
+
const workspace = getActiveWorkspace();
|
|
150
|
+
|
|
151
|
+
if (!workspace.jira_url) {
|
|
152
|
+
console.error("Error: jira_url not configured");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
if (!workspace.jira_email) {
|
|
156
|
+
console.error("Error: jira_email not configured");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
if (!workspace.jira_token) {
|
|
160
|
+
console.error("Error: jira_token not configured");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
url: workspace.jira_url.replace(/\/$/, ""),
|
|
166
|
+
email: workspace.jira_email,
|
|
167
|
+
token: workspace.jira_token,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Make a GitHub API request
|
|
173
|
+
*/
|
|
174
|
+
export async function githubRequest<T>(
|
|
175
|
+
endpoint: string,
|
|
176
|
+
options: RequestInit = {}
|
|
177
|
+
): Promise<T> {
|
|
178
|
+
const token = getGitHubToken();
|
|
179
|
+
|
|
180
|
+
console.log(`[GitHub API] Requesting: ${endpoint}`);
|
|
181
|
+
|
|
182
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
183
|
+
...options,
|
|
184
|
+
headers: {
|
|
185
|
+
Authorization: `Bearer ${token}`,
|
|
186
|
+
Accept: "application/vnd.github+json",
|
|
187
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
...options.headers,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const error = await response.json().catch(() => ({}));
|
|
195
|
+
console.error(`Error: ${response.status} - ${response.statusText}`);
|
|
196
|
+
if (error.message) console.error(` ${error.message}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return response.json() as Promise<T>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Make a JIRA API request
|
|
205
|
+
*/
|
|
206
|
+
export async function jiraRequest<T>(
|
|
207
|
+
endpoint: string,
|
|
208
|
+
options: RequestInit = {}
|
|
209
|
+
): Promise<T> {
|
|
210
|
+
const { url, email, token } = getJiraConfig();
|
|
211
|
+
const auth = Buffer.from(`${email}:${token}`).toString("base64");
|
|
212
|
+
|
|
213
|
+
const response = await fetch(`${url}${endpoint}`, {
|
|
214
|
+
...options,
|
|
215
|
+
headers: {
|
|
216
|
+
Authorization: `Basic ${auth}`,
|
|
217
|
+
Accept: "application/json",
|
|
218
|
+
"Content-Type": "application/json",
|
|
219
|
+
...options.headers,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
const error = await response.json().catch(() => ({}));
|
|
225
|
+
console.error(`Error: ${response.status} - ${response.statusText}`);
|
|
226
|
+
if (error.errorMessages) console.error(` ${error.errorMessages}`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle 204 No Content
|
|
231
|
+
if (response.status === 204) {
|
|
232
|
+
return {} as T;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return response.json() as Promise<T>;
|
|
236
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "brn-toolkit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI Developer Workflow Toolkit - Skills for Claude Code and Copilot CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "zfael",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/zfael/brn.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"bin": {
|
|
14
|
+
"brn": "./dist/cli/brn.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"skills",
|
|
19
|
+
"lib",
|
|
20
|
+
"cli",
|
|
21
|
+
"README.md",
|
|
22
|
+
"GEMINI.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
27
|
+
"setup": "npx tsx cli/brn.ts setup",
|
|
28
|
+
"github:repos": "npx tsx skills/github/scripts/list_repos.ts",
|
|
29
|
+
"github:repo": "npx tsx skills/github/scripts/get_repo_info.ts",
|
|
30
|
+
"github:pr:create": "npx tsx skills/github/scripts/create_pr.ts",
|
|
31
|
+
"github:pr:list": "npx tsx skills/github/scripts/list_prs.ts",
|
|
32
|
+
"jira:tickets": "npx tsx skills/jira/scripts/list_tickets.ts",
|
|
33
|
+
"jira:ticket": "npx tsx skills/jira/scripts/get_ticket.ts",
|
|
34
|
+
"jira:update": "npx tsx skills/jira/scripts/update_ticket.ts",
|
|
35
|
+
"jira:comment": "npx tsx skills/jira/scripts/add_comment.ts"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"tsx": "^4.7.0",
|
|
39
|
+
"yaml": "^2.3.4",
|
|
40
|
+
"zx": "^8.1.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.10.0",
|
|
44
|
+
"typescript": "^5.3.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brn:git-worktree
|
|
3
|
+
description: |
|
|
4
|
+
Manage git repositories using the worktree pattern.
|
|
5
|
+
This allows multiple branches to be checked out simultaneously in sibling directories.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Git Worktree Manager
|
|
9
|
+
|
|
10
|
+
## Description
|
|
11
|
+
The git-worktree skill manages repositories using a specific directory structure optimized for multi-tasking. Instead of a single `.git` folder with one working directory, it uses a "bare" clone (or similar) and creates a separate directory for each branch/feature.
|
|
12
|
+
|
|
13
|
+
## Available Scripts
|
|
14
|
+
|
|
15
|
+
### `clone_repo`
|
|
16
|
+
Clones a repository into the current workspace, setting it up for worktree usage. This creates a directory structure where the main repo is stored, and worktrees can be added alongside it.
|
|
17
|
+
|
|
18
|
+
* **Usage**: `brn git-worktree clone_repo <repo_url> [name]`
|
|
19
|
+
* **Arguments**:
|
|
20
|
+
* `repo_url`: The URL of the git repository to clone.
|
|
21
|
+
* `name` (optional): The name of the directory to create. Defaults to the repo name from the URL.
|
|
22
|
+
* **Example**: `brn git-worktree clone_repo https://github.com/myorg/api.git`
|
|
23
|
+
|
|
24
|
+
### `create_worktree`
|
|
25
|
+
Creates a new worktree for a specific branch. If the branch doesn't exist, it can be created from a base branch.
|
|
26
|
+
|
|
27
|
+
* **Usage**: `brn git-worktree create_worktree <repo_name> <branch_name> [base_branch]`
|
|
28
|
+
* **Arguments**:
|
|
29
|
+
* `repo_name`: The name of the repository (must be already cloned via `clone_repo`).
|
|
30
|
+
* `branch_name`: The name of the new branch and worktree directory.
|
|
31
|
+
* `base_branch` (optional): The branch to split from. Defaults to `main` or `master`.
|
|
32
|
+
* **Example**: `brn git-worktree create_worktree api feature/login`
|
|
33
|
+
|
|
34
|
+
### `list_worktrees`
|
|
35
|
+
Lists all active worktrees for a repository.
|
|
36
|
+
|
|
37
|
+
* **Usage**: `brn git-worktree list_worktrees <repo_name>`
|
|
38
|
+
* **Arguments**:
|
|
39
|
+
* `repo_name`: The name of the repository to list worktrees for.
|
|
40
|
+
* **Example**: `brn git-worktree list_worktrees api`
|
|
41
|
+
|
|
42
|
+
### `remove_worktree`
|
|
43
|
+
Removes a worktree and its directory. This cleans up the working directory and the git worktree entry.
|
|
44
|
+
|
|
45
|
+
* **Usage**: `brn git-worktree remove_worktree <repo_name> <branch_name>`
|
|
46
|
+
* **Arguments**:
|
|
47
|
+
* `repo_name`: The name of the repository.
|
|
48
|
+
* `branch_name`: The name of the worktree/branch to remove.
|
|
49
|
+
* **Example**: `brn git-worktree remove_worktree api feature/login`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Clone a repository to the active workspace
|
|
3
|
+
# Usage: clone_repo.sh <repo_url> [name]
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
REPO_URL="$1"
|
|
8
|
+
REPO_NAME="$2"
|
|
9
|
+
|
|
10
|
+
if [ -z "$REPO_URL" ]; then
|
|
11
|
+
echo "Usage: clone_repo.sh <repo_url> [name]"
|
|
12
|
+
echo "Example: clone_repo.sh https://github.com/org/my-app.git"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Get the script directory to find workspace-manager
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
18
|
+
WORKSPACE_SCRIPT="$SCRIPT_DIR/../../workspace-manager/scripts/get_active_workspace.sh"
|
|
19
|
+
|
|
20
|
+
if [ ! -f "$WORKSPACE_SCRIPT" ]; then
|
|
21
|
+
echo "Error: workspace-manager skill not found"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Get workspace config
|
|
26
|
+
WORKSPACE_JSON=$("$WORKSPACE_SCRIPT" --json)
|
|
27
|
+
WORK_DIR=$(echo "$WORKSPACE_JSON" | yq -r '.path')
|
|
28
|
+
WORK_DIR="${WORK_DIR/#\~/$HOME}"
|
|
29
|
+
|
|
30
|
+
# Derive repo name from URL if not provided
|
|
31
|
+
if [ -z "$REPO_NAME" ]; then
|
|
32
|
+
REPO_NAME=$(basename "$REPO_URL" .git)
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
REPO_PATH="$WORK_DIR/$REPO_NAME"
|
|
36
|
+
|
|
37
|
+
if [ -d "$REPO_PATH" ]; then
|
|
38
|
+
echo "Repository already exists at $REPO_PATH"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Create work directory if needed
|
|
43
|
+
mkdir -p "$WORK_DIR"
|
|
44
|
+
|
|
45
|
+
# Clone the repository
|
|
46
|
+
echo "Cloning $REPO_URL to $REPO_PATH..."
|
|
47
|
+
git clone "$REPO_URL" "$REPO_PATH"
|
|
48
|
+
|
|
49
|
+
# Create worktrees directory
|
|
50
|
+
mkdir -p "$WORK_DIR/${REPO_NAME}-worktrees"
|
|
51
|
+
|
|
52
|
+
echo "✓ Cloned '$REPO_NAME' to $REPO_PATH"
|
|
53
|
+
echo " Worktrees will be created in: $WORK_DIR/${REPO_NAME}-worktrees/"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Create a new worktree for a branch
|
|
3
|
+
# Usage: create_worktree.sh <repo_name> <branch_name> [base_branch]
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
REPO_NAME="$1"
|
|
8
|
+
BRANCH_NAME="$2"
|
|
9
|
+
BASE_BRANCH="${3:-main}"
|
|
10
|
+
|
|
11
|
+
if [ -z "$REPO_NAME" ] || [ -z "$BRANCH_NAME" ]; then
|
|
12
|
+
echo "Usage: create_worktree.sh <repo_name> <branch_name> [base_branch]"
|
|
13
|
+
echo "Example: create_worktree.sh my-app PROJ-123-feature main"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Get the script directory to find workspace-manager
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
|
+
WORKSPACE_SCRIPT="$SCRIPT_DIR/../../workspace-manager/scripts/get_active_workspace.sh"
|
|
20
|
+
|
|
21
|
+
if [ ! -f "$WORKSPACE_SCRIPT" ]; then
|
|
22
|
+
echo "Error: workspace-manager skill not found"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Get workspace config
|
|
27
|
+
WORKSPACE_JSON=$("$WORKSPACE_SCRIPT" --json)
|
|
28
|
+
WORK_DIR=$(echo "$WORKSPACE_JSON" | yq -r '.path')
|
|
29
|
+
WORK_DIR="${WORK_DIR/#\~/$HOME}"
|
|
30
|
+
|
|
31
|
+
REPO_PATH="$WORK_DIR/$REPO_NAME"
|
|
32
|
+
WORKTREES_DIR="$WORK_DIR/${REPO_NAME}-worktrees"
|
|
33
|
+
WORKTREE_PATH="$WORKTREES_DIR/$BRANCH_NAME"
|
|
34
|
+
|
|
35
|
+
if [ ! -d "$REPO_PATH" ]; then
|
|
36
|
+
echo "Error: Repository not found at $REPO_PATH"
|
|
37
|
+
echo "Run clone_repo.sh first"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if [ -d "$WORKTREE_PATH" ]; then
|
|
42
|
+
echo "Worktree already exists at $WORKTREE_PATH"
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Ensure worktrees directory exists
|
|
47
|
+
mkdir -p "$WORKTREES_DIR"
|
|
48
|
+
|
|
49
|
+
# Go to the main repo
|
|
50
|
+
cd "$REPO_PATH"
|
|
51
|
+
|
|
52
|
+
# Fetch latest
|
|
53
|
+
echo "Fetching latest from origin..."
|
|
54
|
+
git fetch origin
|
|
55
|
+
|
|
56
|
+
# Try to checkout existing remote branch, or create new one from base
|
|
57
|
+
if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
|
|
58
|
+
echo "Creating worktree from existing remote branch..."
|
|
59
|
+
git worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
|
|
60
|
+
else
|
|
61
|
+
echo "Creating new branch '$BRANCH_NAME' from '$BASE_BRANCH'..."
|
|
62
|
+
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" "origin/$BASE_BRANCH"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
echo "✓ Created worktree at $WORKTREE_PATH"
|
|
66
|
+
echo " cd $WORKTREE_PATH"
|