333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
import process from "node:process";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import { Command } from "commander";
|
|
|
|
import { createConfluenceClient } from "./confluence.js";
|
|
import { loadConfig } from "./config.js";
|
|
import { readWorkspaceFile } from "./files.js";
|
|
import { runHealthCheck } from "./health.js";
|
|
import { createJiraClient } from "./jira.js";
|
|
import { writeOutput } from "./output.js";
|
|
import { runRawCommand } from "./raw.js";
|
|
import type { FetchLike, OutputFormat, Writer } from "./types.js";
|
|
|
|
type CliContext = {
|
|
cwd?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
fetchImpl?: FetchLike;
|
|
stdout?: Writer;
|
|
stderr?: Writer;
|
|
};
|
|
|
|
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;
|
|
let confluenceCache: ReturnType<typeof createConfluenceClient> | undefined;
|
|
|
|
function getConfig() {
|
|
configCache ??= loadConfig(env, { cwd });
|
|
return configCache;
|
|
}
|
|
|
|
function getJiraClient() {
|
|
jiraCache ??= createJiraClient({
|
|
config: getConfig(),
|
|
fetchImpl: context.fetchImpl,
|
|
});
|
|
return jiraCache;
|
|
}
|
|
|
|
function getConfluenceClient() {
|
|
confluenceCache ??= createConfluenceClient({
|
|
config: getConfig(),
|
|
fetchImpl: context.fetchImpl,
|
|
});
|
|
return confluenceCache;
|
|
}
|
|
|
|
async function readBodyFile(filePath: string | undefined) {
|
|
if (!filePath) {
|
|
return undefined;
|
|
}
|
|
|
|
return readWorkspaceFile(filePath, cwd);
|
|
}
|
|
|
|
return {
|
|
cwd,
|
|
stdout,
|
|
stderr,
|
|
readBodyFile,
|
|
getConfig,
|
|
getJiraClient,
|
|
getConfluenceClient,
|
|
fetchImpl: context.fetchImpl,
|
|
};
|
|
}
|
|
|
|
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(async (options) => {
|
|
const payload = await runHealthCheck(runtime.getConfig(), runtime.fetchImpl);
|
|
writeOutput(
|
|
runtime.stdout,
|
|
payload,
|
|
resolveFormat(options.format),
|
|
);
|
|
});
|
|
|
|
program
|
|
.command("conf-search")
|
|
.requiredOption("--query <query>", "CQL search query")
|
|
.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.getConfluenceClient().searchPages({
|
|
query: options.query,
|
|
maxResults: Number(options.maxResults),
|
|
startAt: Number(options.startAt),
|
|
});
|
|
|
|
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
|
});
|
|
|
|
program
|
|
.command("conf-get")
|
|
.requiredOption("--page <page>", "Confluence page ID")
|
|
.option("--format <format>", "Output format", "json")
|
|
.action(async (options) => {
|
|
const payload = await runtime.getConfluenceClient().getPage(options.page);
|
|
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
|
});
|
|
|
|
program
|
|
.command("conf-create")
|
|
.requiredOption("--title <title>", "Confluence page title")
|
|
.requiredOption("--body-file <path>", "Workspace-relative storage-format body file")
|
|
.option("--space <space>", "Confluence space ID")
|
|
.option("--dry-run", "Print the request without sending it")
|
|
.option("--format <format>", "Output format", "json")
|
|
.action(async (options) => {
|
|
const payload = await runtime.getConfluenceClient().createPage({
|
|
space: options.space,
|
|
title: options.title,
|
|
body: (await runtime.readBodyFile(options.bodyFile)) as string,
|
|
dryRun: Boolean(options.dryRun),
|
|
});
|
|
|
|
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
|
});
|
|
|
|
program
|
|
.command("conf-update")
|
|
.requiredOption("--page <page>", "Confluence page ID")
|
|
.requiredOption("--title <title>", "Confluence page title")
|
|
.requiredOption("--body-file <path>", "Workspace-relative storage-format body file")
|
|
.option("--dry-run", "Print the request without sending it")
|
|
.option("--format <format>", "Output format", "json")
|
|
.action(async (options) => {
|
|
const payload = await runtime.getConfluenceClient().updatePage({
|
|
pageId: options.page,
|
|
title: options.title,
|
|
body: (await runtime.readBodyFile(options.bodyFile)) as string,
|
|
dryRun: Boolean(options.dryRun),
|
|
});
|
|
|
|
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
|
});
|
|
|
|
program
|
|
.command("conf-comment")
|
|
.requiredOption("--page <page>", "Confluence page ID")
|
|
.requiredOption("--body-file <path>", "Workspace-relative storage-format body file")
|
|
.option("--dry-run", "Print the request without sending it")
|
|
.option("--format <format>", "Output format", "json")
|
|
.action(async (options) => {
|
|
const payload = await runtime.getConfluenceClient().commentPage({
|
|
pageId: options.page,
|
|
body: (await runtime.readBodyFile(options.bodyFile)) as string,
|
|
dryRun: Boolean(options.dryRun),
|
|
});
|
|
|
|
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
|
});
|
|
|
|
program
|
|
.command("conf-children")
|
|
.requiredOption("--page <page>", "Confluence page ID")
|
|
.option("--max-results <number>", "Maximum results to return", "50")
|
|
.option("--start-at <number>", "Cursor/start token", "0")
|
|
.option("--format <format>", "Output format", "json")
|
|
.action(async (options) => {
|
|
const payload = await runtime.getConfluenceClient().listChildren(
|
|
options.page,
|
|
Number(options.maxResults),
|
|
Number(options.startAt),
|
|
);
|
|
|
|
writeOutput(runtime.stdout, payload, resolveFormat(options.format));
|
|
});
|
|
|
|
program
|
|
.command("raw")
|
|
.requiredOption("--product <product>", "jira or confluence")
|
|
.requiredOption("--method <method>", "GET, POST, or PUT")
|
|
.requiredOption("--path <path>", "Validated API path")
|
|
.option("--body-file <path>", "Workspace-relative JSON file")
|
|
.option("--dry-run", "Print the request without sending it")
|
|
.option("--format <format>", "Output format", "json")
|
|
.action(async (options) => {
|
|
const payload = await runRawCommand(runtime.getConfig(), runtime.fetchImpl, {
|
|
product: options.product,
|
|
method: String(options.method).toUpperCase(),
|
|
path: options.path,
|
|
bodyFile: options.bodyFile,
|
|
cwd: runtime.cwd,
|
|
dryRun: Boolean(options.dryRun),
|
|
});
|
|
|
|
writeOutput(runtime.stdout, payload, 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;
|
|
});
|
|
}
|