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
package/dist/cli/brn.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BRN-Toolkit CLI - Main entry point
|
|
4
|
+
* Usage: brn <command> [options]
|
|
5
|
+
*/
|
|
6
|
+
import { $ } from "zx";
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join, dirname } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import * as readline from "readline";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
$.verbose = false;
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
// Handle both source (cli/brn.ts) and compiled (dist/cli/brn.js) locations
|
|
17
|
+
const isCompiled = __dirname.includes(join("dist", "cli"));
|
|
18
|
+
const PROJECT_ROOT = isCompiled ? join(__dirname, "..", "..") : join(__dirname, "..");
|
|
19
|
+
const BRN_DIR = join(homedir(), ".brn");
|
|
20
|
+
const CONFIG_FILE = join(BRN_DIR, "config.yaml");
|
|
21
|
+
// Simple readline interface for prompts
|
|
22
|
+
function prompt(question) {
|
|
23
|
+
const rl = readline.createInterface({
|
|
24
|
+
input: process.stdin,
|
|
25
|
+
output: process.stdout,
|
|
26
|
+
});
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
rl.question(question, (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(answer.trim());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function runSkill(skill, script, args) {
|
|
35
|
+
// Determine where to look for scripts
|
|
36
|
+
// Prefer dist/skills/.../*.js for speed, fallback to skills/.../*.ts
|
|
37
|
+
const distScriptsDir = join(PROJECT_ROOT, "dist", "skills", skill, "scripts");
|
|
38
|
+
const srcScriptsDir = join(PROJECT_ROOT, "skills", skill, "scripts");
|
|
39
|
+
const jsPath = join(distScriptsDir, `${script}.js`);
|
|
40
|
+
const tsPath = join(srcScriptsDir, `${script}.ts`);
|
|
41
|
+
const shPath = join(srcScriptsDir, `${script}.sh`);
|
|
42
|
+
// Enable verbose to show output of the child script
|
|
43
|
+
const previousVerbose = $.verbose;
|
|
44
|
+
$.verbose = true;
|
|
45
|
+
try {
|
|
46
|
+
if (existsSync(jsPath)) {
|
|
47
|
+
// Run compiled JS with node
|
|
48
|
+
await $ `node ${jsPath} ${args}`;
|
|
49
|
+
}
|
|
50
|
+
else if (existsSync(tsPath)) {
|
|
51
|
+
// Run TS with tsx
|
|
52
|
+
await $ `npx tsx ${tsPath} ${args}`;
|
|
53
|
+
}
|
|
54
|
+
else if (existsSync(shPath)) {
|
|
55
|
+
// Run shell
|
|
56
|
+
await $ `bash ${shPath} ${args}`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Try without extension in src
|
|
60
|
+
const exactPath = join(srcScriptsDir, script);
|
|
61
|
+
if (existsSync(exactPath)) {
|
|
62
|
+
if (script.endsWith(".sh"))
|
|
63
|
+
await $ `bash ${exactPath} ${args}`;
|
|
64
|
+
else
|
|
65
|
+
await $ `npx tsx ${exactPath} ${args}`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error(`Skill script not found: ${skill}/${script}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
$.verbose = previousVerbose;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function setup() {
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log("🚀 BRN Setup Wizard");
|
|
80
|
+
console.log("===================");
|
|
81
|
+
console.log("");
|
|
82
|
+
// Create config directory
|
|
83
|
+
if (!existsSync(BRN_DIR)) {
|
|
84
|
+
mkdirSync(BRN_DIR, { recursive: true });
|
|
85
|
+
console.log(`✓ Created ${BRN_DIR}`);
|
|
86
|
+
}
|
|
87
|
+
// Check for existing config
|
|
88
|
+
let config = { version: "1.0", active_workspace: "", workspaces: {} };
|
|
89
|
+
if (existsSync(CONFIG_FILE)) {
|
|
90
|
+
config = YAML.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
91
|
+
console.log(`✓ Found existing config with ${Object.keys(config.workspaces).length} workspace(s)`);
|
|
92
|
+
console.log("");
|
|
93
|
+
}
|
|
94
|
+
// Workspace name
|
|
95
|
+
const defaultName = Object.keys(config.workspaces).length === 0 ? "personal" : "";
|
|
96
|
+
const name = await prompt(`Workspace name [${defaultName}]: `) || defaultName;
|
|
97
|
+
if (!name) {
|
|
98
|
+
console.log("❌ Workspace name is required");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
// Work directory
|
|
102
|
+
const defaultPath = join(homedir(), "dev", name, "auto");
|
|
103
|
+
const workDir = await prompt(`Work directory [${defaultPath}]: `) || defaultPath;
|
|
104
|
+
// GitHub token (optional)
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log("📦 GitHub Configuration (optional, press Enter to skip)");
|
|
107
|
+
const githubToken = await prompt("GitHub token: ");
|
|
108
|
+
const githubOrg = await prompt("Default GitHub Organization/User: ");
|
|
109
|
+
// JIRA config (optional)
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log("📋 JIRA Configuration (optional, press Enter to skip)");
|
|
112
|
+
const jiraUrl = await prompt("JIRA URL (e.g., https://company.atlassian.net): ");
|
|
113
|
+
let jiraEmail = "";
|
|
114
|
+
let jiraToken = "";
|
|
115
|
+
if (jiraUrl) {
|
|
116
|
+
jiraEmail = await prompt("JIRA email: ");
|
|
117
|
+
jiraToken = await prompt("JIRA API token: ");
|
|
118
|
+
}
|
|
119
|
+
// Automation settings
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log("⚙️ Automation Settings");
|
|
122
|
+
console.log(" By default, all automation is OFF for safety.");
|
|
123
|
+
const enableAuto = await prompt("Enable any automation? (y/N): ");
|
|
124
|
+
let automation = {
|
|
125
|
+
github_auto_push: false,
|
|
126
|
+
github_auto_pr: false,
|
|
127
|
+
jira_auto_transition: false,
|
|
128
|
+
jira_auto_comment: false,
|
|
129
|
+
};
|
|
130
|
+
if (enableAuto.toLowerCase() === "y") {
|
|
131
|
+
const autoPush = await prompt(" Auto-push commits? (y/N): ");
|
|
132
|
+
const autoPr = await prompt(" Auto-create PRs? (y/N): ");
|
|
133
|
+
const autoTransition = await prompt(" Auto-update JIRA status? (y/N): ");
|
|
134
|
+
const autoComment = await prompt(" Auto-add JIRA comments? (y/N): ");
|
|
135
|
+
automation = {
|
|
136
|
+
github_auto_push: autoPush.toLowerCase() === "y",
|
|
137
|
+
github_auto_pr: autoPr.toLowerCase() === "y",
|
|
138
|
+
jira_auto_transition: autoTransition.toLowerCase() === "y",
|
|
139
|
+
jira_auto_comment: autoComment.toLowerCase() === "y",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Build workspace config
|
|
143
|
+
const workspace = {
|
|
144
|
+
path: workDir,
|
|
145
|
+
automation,
|
|
146
|
+
};
|
|
147
|
+
if (githubToken)
|
|
148
|
+
workspace.github_token = githubToken;
|
|
149
|
+
if (githubOrg)
|
|
150
|
+
workspace.github_org = githubOrg;
|
|
151
|
+
if (jiraUrl) {
|
|
152
|
+
workspace.jira_url = jiraUrl;
|
|
153
|
+
workspace.jira_email = jiraEmail;
|
|
154
|
+
workspace.jira_token = jiraToken;
|
|
155
|
+
}
|
|
156
|
+
// Save config
|
|
157
|
+
config.workspaces[name] = workspace;
|
|
158
|
+
if (!config.active_workspace) {
|
|
159
|
+
config.active_workspace = name;
|
|
160
|
+
}
|
|
161
|
+
writeFileSync(CONFIG_FILE, YAML.stringify(config));
|
|
162
|
+
// Create work directory
|
|
163
|
+
if (!existsSync(workDir)) {
|
|
164
|
+
mkdirSync(workDir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
console.log("");
|
|
167
|
+
console.log("✅ Setup complete!");
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log(` Workspace: ${name}`);
|
|
170
|
+
console.log(` Work dir: ${workDir}`);
|
|
171
|
+
console.log(` Config: ${CONFIG_FILE}`);
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log("Next steps:");
|
|
174
|
+
console.log(" • Use 'brn workspace list' to see workspaces");
|
|
175
|
+
console.log(" • Use 'brn workspace switch <name>' to change workspace");
|
|
176
|
+
console.log(" • Ask your AI assistant to 'list my JIRA tickets'");
|
|
177
|
+
console.log("");
|
|
178
|
+
}
|
|
179
|
+
async function workspaceList() {
|
|
180
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
181
|
+
console.log("No workspaces configured. Run: brn setup");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const config = YAML.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
185
|
+
const active = config.active_workspace;
|
|
186
|
+
console.log("Workspaces:");
|
|
187
|
+
console.log("===========");
|
|
188
|
+
for (const [name, ws] of Object.entries(config.workspaces)) {
|
|
189
|
+
const workspace = ws;
|
|
190
|
+
const marker = name === active ? "* " : " ";
|
|
191
|
+
const activeLabel = name === active ? " [ACTIVE]" : "";
|
|
192
|
+
console.log(`${marker}${name} (${workspace.path})${activeLabel}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function workspaceSwitch(name) {
|
|
196
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
197
|
+
console.log("No workspaces configured. Run: brn setup");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const config = YAML.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
201
|
+
if (!config.workspaces[name]) {
|
|
202
|
+
console.log(`Workspace '${name}' not found.`);
|
|
203
|
+
console.log("Available:", Object.keys(config.workspaces).join(", "));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
config.active_workspace = name;
|
|
207
|
+
writeFileSync(CONFIG_FILE, YAML.stringify(config));
|
|
208
|
+
console.log(`✓ Switched to workspace '${name}'`);
|
|
209
|
+
}
|
|
210
|
+
function showHelp() {
|
|
211
|
+
console.log(`
|
|
212
|
+
BRN - AI Developer Workflow Toolkit
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
brn <command> [options]
|
|
216
|
+
brn <skill> <script> [args]
|
|
217
|
+
|
|
218
|
+
Commands:
|
|
219
|
+
start <ticket> Shortcut for workflow start <ticket>
|
|
220
|
+
setup Interactive setup wizard
|
|
221
|
+
workspace list List all workspaces
|
|
222
|
+
workspace switch <n> Switch to workspace <n>
|
|
223
|
+
help Show this help
|
|
224
|
+
|
|
225
|
+
Skills:
|
|
226
|
+
github <script> Run a GitHub skill script (e.g., list_repos)
|
|
227
|
+
jira <script> Run a JIRA skill script (e.g., list_tickets)
|
|
228
|
+
git-worktree <script> Run a Git Worktree skill script (e.g., clone_repo)
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
brn start PROJ-123
|
|
232
|
+
brn github list_repos
|
|
233
|
+
brn jira list_tickets
|
|
234
|
+
brn workspace switch work
|
|
235
|
+
`);
|
|
236
|
+
}
|
|
237
|
+
// Main
|
|
238
|
+
const args = process.argv.slice(2);
|
|
239
|
+
const command = args[0];
|
|
240
|
+
switch (command) {
|
|
241
|
+
case "start":
|
|
242
|
+
await runSkill("workflow", "start", args.slice(1));
|
|
243
|
+
break;
|
|
244
|
+
case "setup":
|
|
245
|
+
await setup();
|
|
246
|
+
break;
|
|
247
|
+
case "workspace":
|
|
248
|
+
if (args[1] === "list") {
|
|
249
|
+
await workspaceList();
|
|
250
|
+
}
|
|
251
|
+
else if (args[1] === "switch" && args[2]) {
|
|
252
|
+
await workspaceSwitch(args[2]);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.log("Usage: brn workspace [list|switch <name>]");
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case "help":
|
|
259
|
+
case "--help":
|
|
260
|
+
case "-h":
|
|
261
|
+
case undefined:
|
|
262
|
+
showHelp();
|
|
263
|
+
break;
|
|
264
|
+
default:
|
|
265
|
+
// Generic skill runner: brn <skill> <script> [...args]
|
|
266
|
+
if (command && args[1]) {
|
|
267
|
+
await runSkill(command, args[1], args.slice(2));
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
console.log(`Unknown command: ${command}`);
|
|
271
|
+
showHelp();
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
$.verbose = false;
|
|
10
|
+
/**
|
|
11
|
+
* Get the brn config from ~/.brn/config.yaml
|
|
12
|
+
*/
|
|
13
|
+
export function getBrnConfig() {
|
|
14
|
+
const configPath = join(homedir(), ".brn", "config.yaml");
|
|
15
|
+
if (!existsSync(configPath)) {
|
|
16
|
+
console.error("Error: No config found at ~/.brn/config.yaml");
|
|
17
|
+
console.error("Run: create_workspace.sh <name> <path>");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const content = readFileSync(configPath, "utf-8");
|
|
21
|
+
return YAML.parse(content);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Save the brn config to ~/.brn/config.yaml
|
|
25
|
+
*/
|
|
26
|
+
export function saveBrnConfig(config) {
|
|
27
|
+
const configPath = join(homedir(), ".brn", "config.yaml");
|
|
28
|
+
writeFileSync(configPath, YAML.stringify(config));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Update the active workspace configuration
|
|
32
|
+
*/
|
|
33
|
+
export function updateWorkspace(updates) {
|
|
34
|
+
const config = getBrnConfig();
|
|
35
|
+
const activeWorkspaceName = config.active_workspace;
|
|
36
|
+
if (!config.workspaces[activeWorkspaceName]) {
|
|
37
|
+
console.error(`Error: Active workspace '${activeWorkspaceName}' not found`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
config.workspaces[activeWorkspaceName] = {
|
|
41
|
+
...config.workspaces[activeWorkspaceName],
|
|
42
|
+
...updates,
|
|
43
|
+
};
|
|
44
|
+
saveBrnConfig(config);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the active workspace config
|
|
48
|
+
*/
|
|
49
|
+
export function getActiveWorkspace() {
|
|
50
|
+
const config = getBrnConfig();
|
|
51
|
+
const name = config.active_workspace;
|
|
52
|
+
const workspace = config.workspaces[name];
|
|
53
|
+
if (!workspace) {
|
|
54
|
+
console.error(`Error: Workspace '${name}' not found in config`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
return { name, ...workspace };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check if an automation action is enabled for the active workspace.
|
|
61
|
+
* All automation is OFF by default for safety.
|
|
62
|
+
*/
|
|
63
|
+
export function isAutomationEnabled(action) {
|
|
64
|
+
const workspace = getActiveWorkspace();
|
|
65
|
+
return workspace.automation?.[action] ?? false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Require automation to be enabled, or prompt user to confirm.
|
|
69
|
+
* Returns true if action should proceed, false if user declined.
|
|
70
|
+
*/
|
|
71
|
+
export function checkAutomation(action, description) {
|
|
72
|
+
if (isAutomationEnabled(action)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
console.log(`⚠️ Automation disabled: ${description}`);
|
|
76
|
+
console.log(` To enable, run: configure_workspace.sh <workspace> automation.${action} true`);
|
|
77
|
+
console.log(` Skipping automatic execution.`);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get GitHub token from active workspace
|
|
82
|
+
*/
|
|
83
|
+
export function getGitHubToken() {
|
|
84
|
+
const workspace = getActiveWorkspace();
|
|
85
|
+
if (!workspace.github_token) {
|
|
86
|
+
console.error("Error: github_token not configured for this workspace");
|
|
87
|
+
console.error(`Run: configure_workspace.sh ${workspace.name} github_token <token>`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
return workspace.github_token;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get JIRA config from active workspace
|
|
94
|
+
*/
|
|
95
|
+
export function getJiraConfig() {
|
|
96
|
+
const workspace = getActiveWorkspace();
|
|
97
|
+
if (!workspace.jira_url) {
|
|
98
|
+
console.error("Error: jira_url not configured");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (!workspace.jira_email) {
|
|
102
|
+
console.error("Error: jira_email not configured");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (!workspace.jira_token) {
|
|
106
|
+
console.error("Error: jira_token not configured");
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
url: workspace.jira_url.replace(/\/$/, ""),
|
|
111
|
+
email: workspace.jira_email,
|
|
112
|
+
token: workspace.jira_token,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Make a GitHub API request
|
|
117
|
+
*/
|
|
118
|
+
export async function githubRequest(endpoint, options = {}) {
|
|
119
|
+
const token = getGitHubToken();
|
|
120
|
+
console.log(`[GitHub API] Requesting: ${endpoint}`);
|
|
121
|
+
const response = await fetch(`https://api.github.com${endpoint}`, {
|
|
122
|
+
...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
|
+
},
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const error = await response.json().catch(() => ({}));
|
|
133
|
+
console.error(`Error: ${response.status} - ${response.statusText}`);
|
|
134
|
+
if (error.message)
|
|
135
|
+
console.error(` ${error.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
return response.json();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Make a JIRA API request
|
|
142
|
+
*/
|
|
143
|
+
export async function jiraRequest(endpoint, options = {}) {
|
|
144
|
+
const { url, email, token } = getJiraConfig();
|
|
145
|
+
const auth = Buffer.from(`${email}:${token}`).toString("base64");
|
|
146
|
+
const response = await fetch(`${url}${endpoint}`, {
|
|
147
|
+
...options,
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Basic ${auth}`,
|
|
150
|
+
Accept: "application/json",
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
...options.headers,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
const error = await response.json().catch(() => ({}));
|
|
157
|
+
console.error(`Error: ${response.status} - ${response.statusText}`);
|
|
158
|
+
if (error.errorMessages)
|
|
159
|
+
console.error(` ${error.errorMessages}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
// Handle 204 No Content
|
|
163
|
+
if (response.status === 204) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
return response.json();
|
|
167
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Create a pull request
|
|
4
|
+
* Usage: npx tsx create_pr.ts <owner/repo> <head> <base> <title> [body]
|
|
5
|
+
*/
|
|
6
|
+
import { githubRequest } from "../../../lib/utils.js";
|
|
7
|
+
const [repoPath, head, base, title, body] = process.argv.slice(2);
|
|
8
|
+
if (!repoPath || !head || !base || !title) {
|
|
9
|
+
console.log("Usage: npx tsx create_pr.ts <owner/repo> <head_branch> <base_branch> <title> [body]");
|
|
10
|
+
console.log("Example: npx tsx create_pr.ts myorg/app feature-123 main 'feat: Add feature'");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const pr = await githubRequest(`/repos/${repoPath}/pulls`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
title,
|
|
17
|
+
head,
|
|
18
|
+
base,
|
|
19
|
+
body: body ?? "",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
console.log(`✓ Created PR #${pr.number}: ${pr.title}`);
|
|
23
|
+
console.log(` URL: ${pr.html_url}`);
|
|
24
|
+
console.log(` ${head} → ${base}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Get repository information
|
|
4
|
+
* Usage: npx tsx get_repo_info.ts <owner/repo>
|
|
5
|
+
*/
|
|
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);
|
|
12
|
+
}
|
|
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}`);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* List pull requests for a repository
|
|
4
|
+
* Usage: npx tsx list_prs.ts <owner/repo> [state]
|
|
5
|
+
*/
|
|
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();
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* List repositories for an organization or user
|
|
4
|
+
* Usage: npx tsx list_repos.ts [org_or_user]
|
|
5
|
+
*/
|
|
6
|
+
import { githubRequest, getActiveWorkspace } from "../../../lib/utils.js";
|
|
7
|
+
let orgOrUser = process.argv[2];
|
|
8
|
+
if (!orgOrUser) {
|
|
9
|
+
const ws = getActiveWorkspace();
|
|
10
|
+
if (ws.github_org) {
|
|
11
|
+
orgOrUser = ws.github_org;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
let endpoint = "/user/repos?per_page=100";
|
|
15
|
+
if (orgOrUser) {
|
|
16
|
+
endpoint = `/orgs/${orgOrUser}/repos?per_page=100`;
|
|
17
|
+
}
|
|
18
|
+
console.log(`Fetching repositories from: ${endpoint}`);
|
|
19
|
+
try {
|
|
20
|
+
let repos = await githubRequest(endpoint);
|
|
21
|
+
// If org fails, try as user
|
|
22
|
+
if (repos.length === 0 && orgOrUser) {
|
|
23
|
+
repos = await githubRequest(`/users/${orgOrUser}/repos?per_page=100`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`Repositories${orgOrUser ? ` for ${orgOrUser}` : ""}:`);
|
|
26
|
+
console.log("=".repeat(50));
|
|
27
|
+
for (const repo of repos) {
|
|
28
|
+
const icon = repo.private ? "🔒" : "🌐";
|
|
29
|
+
console.log(`${icon} ${repo.full_name}`);
|
|
30
|
+
if (repo.description) {
|
|
31
|
+
console.log(` ${repo.description.slice(0, 60)}...`);
|
|
32
|
+
}
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Failed to list repositories:", error);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Add a comment to a JIRA ticket
|
|
4
|
+
* Usage: npx tsx add_comment.ts <ticket_key> <comment>
|
|
5
|
+
*/
|
|
6
|
+
import { jiraRequest } from "../../../lib/utils.js";
|
|
7
|
+
const ticketKey = process.argv[2];
|
|
8
|
+
const commentText = process.argv.slice(3).join(" ");
|
|
9
|
+
if (!ticketKey || !commentText) {
|
|
10
|
+
console.log("Usage: npx tsx add_comment.ts <ticket_key> <comment>");
|
|
11
|
+
console.log("Example: npx tsx add_comment.ts PROJ-123 'PR created: https://...'");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
// JIRA API v3 uses ADF format for comments
|
|
15
|
+
const body = {
|
|
16
|
+
body: {
|
|
17
|
+
type: "doc",
|
|
18
|
+
version: 1,
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: "paragraph",
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: commentText,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
await jiraRequest(`/rest/api/3/issue/${ticketKey}/comment`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
});
|
|
36
|
+
console.log(`✓ Added comment to ${ticketKey}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Get JIRA ticket details
|
|
4
|
+
* Usage: npx tsx get_ticket.ts <ticket_key>
|
|
5
|
+
*/
|
|
6
|
+
import { jiraRequest, getJiraConfig } from "../../../lib/utils.js";
|
|
7
|
+
function extractText(node) {
|
|
8
|
+
if (typeof node !== "object" || node === null)
|
|
9
|
+
return "";
|
|
10
|
+
const obj = node;
|
|
11
|
+
if (obj.type === "text" && typeof obj.text === "string") {
|
|
12
|
+
return obj.text;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(obj.content)) {
|
|
15
|
+
return obj.content.map(extractText).join(" ");
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
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));
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(`URL: ${url}/browse/${issue.key}`);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* List JIRA tickets assigned to current user
|
|
4
|
+
* Usage: npx tsx list_tickets.ts [status]
|
|
5
|
+
*/
|
|
6
|
+
import { jiraRequest } from "../../../lib/utils.js";
|
|
7
|
+
const statusFilter = process.argv[2];
|
|
8
|
+
let jql = "assignee = currentUser() ORDER BY updated DESC";
|
|
9
|
+
if (statusFilter) {
|
|
10
|
+
jql = `assignee = currentUser() AND status = "${statusFilter}" ORDER BY updated DESC`;
|
|
11
|
+
}
|
|
12
|
+
const data = await jiraRequest("/rest/api/3/search/jql", {
|
|
13
|
+
method: "POST",
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
jql,
|
|
16
|
+
maxResults: 20,
|
|
17
|
+
fields: ["summary", "status", "priority"],
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
const issues = data.issues;
|
|
21
|
+
console.log(`Your Tickets (${issues.length} found):`);
|
|
22
|
+
console.log("=".repeat(60));
|
|
23
|
+
if (issues.length === 0) {
|
|
24
|
+
console.log("No tickets found.");
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const statusEmoji = {
|
|
28
|
+
"To Do": "📋",
|
|
29
|
+
"In Progress": "🔧",
|
|
30
|
+
"Code Review": "👀",
|
|
31
|
+
Done: "✅",
|
|
32
|
+
};
|
|
33
|
+
for (const issue of issues) {
|
|
34
|
+
const { key, fields } = issue;
|
|
35
|
+
const summary = fields.summary.slice(0, 50);
|
|
36
|
+
const status = fields.status.name;
|
|
37
|
+
const priority = fields.priority?.name ?? "None";
|
|
38
|
+
const emoji = statusEmoji[status] ?? "📌";
|
|
39
|
+
console.log(`${emoji} ${key}: ${summary}`);
|
|
40
|
+
console.log(` Status: ${status} | Priority: ${priority}`);
|
|
41
|
+
console.log();
|
|
42
|
+
}
|