feat(atlassian): implement milestone M2 - jira command surface
This commit is contained in:
@@ -1,22 +1,212 @@
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { Command } from "commander";
|
||||
|
||||
const program = new Command()
|
||||
.name("atlassian")
|
||||
.description("Portable Atlassian CLI for multi-agent skills")
|
||||
.version("0.1.0");
|
||||
import { loadConfig } from "./config.js";
|
||||
import { readWorkspaceFile } from "./files.js";
|
||||
import { createJiraClient } from "./jira.js";
|
||||
import { writeOutput } from "./output.js";
|
||||
import type { FetchLike, OutputFormat, Writer } from "./types.js";
|
||||
|
||||
program
|
||||
.command("health")
|
||||
.description("Validate configuration and Atlassian connectivity")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action((options) => {
|
||||
const payload = {
|
||||
ok: false,
|
||||
message: "health is not implemented yet",
|
||||
format: options.format,
|
||||
};
|
||||
type CliContext = {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
fetchImpl?: FetchLike;
|
||||
stdout?: Writer;
|
||||
stderr?: Writer;
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
function resolveFormat(format: string | undefined): OutputFormat {
|
||||
return format === "text" ? "text" : "json";
|
||||
}
|
||||
|
||||
function createRuntime(context: CliContext) {
|
||||
const cwd = context.cwd ?? process.cwd();
|
||||
const env = context.env ?? process.env;
|
||||
const stdout = context.stdout ?? process.stdout;
|
||||
const stderr = context.stderr ?? process.stderr;
|
||||
let configCache: ReturnType<typeof loadConfig> | undefined;
|
||||
let jiraCache: ReturnType<typeof createJiraClient> | undefined;
|
||||
|
||||
function getConfig() {
|
||||
configCache ??= loadConfig(env, { cwd });
|
||||
return configCache;
|
||||
}
|
||||
|
||||
function getJiraClient() {
|
||||
jiraCache ??= createJiraClient({
|
||||
config: getConfig(),
|
||||
fetchImpl: context.fetchImpl,
|
||||
});
|
||||
return jiraCache;
|
||||
}
|
||||
|
||||
async function readBodyFile(filePath: string | undefined) {
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return readWorkspaceFile(filePath, cwd);
|
||||
}
|
||||
|
||||
return {
|
||||
cwd,
|
||||
stdout,
|
||||
stderr,
|
||||
readBodyFile,
|
||||
getConfig,
|
||||
getJiraClient,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildProgram(context: CliContext = {}) {
|
||||
const runtime = createRuntime(context);
|
||||
const program = new Command()
|
||||
.name("atlassian")
|
||||
.description("Portable Atlassian CLI for multi-agent skills")
|
||||
.version("0.1.0");
|
||||
|
||||
program
|
||||
.command("health")
|
||||
.description("Validate configuration and Atlassian connectivity")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action((options) => {
|
||||
writeOutput(
|
||||
runtime.stdout,
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
baseUrl: runtime.getConfig().baseUrl,
|
||||
jiraBaseUrl: runtime.getConfig().jiraBaseUrl,
|
||||
confluenceBaseUrl: runtime.getConfig().confluenceBaseUrl,
|
||||
defaultProject: runtime.getConfig().defaultProject,
|
||||
defaultSpace: runtime.getConfig().defaultSpace,
|
||||
},
|
||||
},
|
||||
resolveFormat(options.format),
|
||||
);
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-search")
|
||||
.requiredOption("--jql <jql>", "JQL expression to execute")
|
||||
.option("--max-results <number>", "Maximum results to return", "50")
|
||||
.option("--start-at <number>", "Result offset", "0")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().searchIssues({
|
||||
jql: options.jql,
|
||||
maxResults: Number(options.maxResults),
|
||||
startAt: Number(options.startAt),
|
||||
});
|
||||
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-get")
|
||||
.requiredOption("--issue <issue>", "Issue key")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().getIssue(options.issue);
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-create")
|
||||
.requiredOption("--type <type>", "Issue type name")
|
||||
.requiredOption("--summary <summary>", "Issue summary")
|
||||
.option("--project <project>", "Project key")
|
||||
.option("--description-file <path>", "Workspace-relative markdown/text file")
|
||||
.option("--dry-run", "Print the request without sending it")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().createIssue({
|
||||
project: options.project,
|
||||
type: options.type,
|
||||
summary: options.summary,
|
||||
description: await runtime.readBodyFile(options.descriptionFile),
|
||||
dryRun: Boolean(options.dryRun),
|
||||
});
|
||||
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-update")
|
||||
.requiredOption("--issue <issue>", "Issue key")
|
||||
.option("--summary <summary>", "Updated summary")
|
||||
.option("--description-file <path>", "Workspace-relative markdown/text file")
|
||||
.option("--dry-run", "Print the request without sending it")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().updateIssue({
|
||||
issue: options.issue,
|
||||
summary: options.summary,
|
||||
description: await runtime.readBodyFile(options.descriptionFile),
|
||||
dryRun: Boolean(options.dryRun),
|
||||
});
|
||||
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-comment")
|
||||
.requiredOption("--issue <issue>", "Issue key")
|
||||
.requiredOption("--body-file <path>", "Workspace-relative markdown/text file")
|
||||
.option("--dry-run", "Print the request without sending it")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().commentIssue({
|
||||
issue: options.issue,
|
||||
body: (await runtime.readBodyFile(options.bodyFile)) as string,
|
||||
dryRun: Boolean(options.dryRun),
|
||||
});
|
||||
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-transitions")
|
||||
.requiredOption("--issue <issue>", "Issue key")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().getTransitions(options.issue);
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
program
|
||||
.command("jira-transition")
|
||||
.requiredOption("--issue <issue>", "Issue key")
|
||||
.requiredOption("--transition <transition>", "Transition ID")
|
||||
.option("--dry-run", "Print the request without sending it")
|
||||
.option("--format <format>", "Output format", "json")
|
||||
.action(async (options) => {
|
||||
const payload = await runtime.getJiraClient().transitionIssue({
|
||||
issue: options.issue,
|
||||
transition: options.transition,
|
||||
dryRun: Boolean(options.dryRun),
|
||||
});
|
||||
|
||||
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
||||
});
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
export async function runCli(argv = process.argv, context: CliContext = {}) {
|
||||
const program = buildProgram(context);
|
||||
await program.parseAsync(argv);
|
||||
}
|
||||
|
||||
const isDirectExecution =
|
||||
Boolean(process.argv[1]) && import.meta.url === pathToFileURL(process.argv[1]).href;
|
||||
|
||||
if (isDirectExecution) {
|
||||
runCli().catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user