251148c3ff
## Summary - add repository-wide quality tooling and verification scaffolding, including CI workflows, pnpm workspace setup, ESLint/Prettier/markdown checks, and generated-output verification helpers - reorganize skill sources and generation flow by introducing canonical `_source` variants, generator/manifests, reusable helper abstractions, and shared web-automation/browser utilities - clean up and expand documentation so the root README flows into docs and skill docs, with clearer development, reviewer, installer, and workflow guidance ## Notable changes - docs flow and consistency cleanup across `README.md`, `docs/README.md`, and related docs - new scripts for `check`, docs verification, generated-file verification, shell portability, and safe directory replacement - refactors in Atlassian and web-automation skill runtimes to reduce duplication and centralize reusable code - changelog, development documentation, and CI surface updates ## Test Plan - [ ] `pnpm run check` - [ ] review generated/manifests and skill sync outputs - [ ] smoke-check docs flow from `README.md` to `docs/README.md` to skill docs ## Notes - this branch currently includes tracked `skills/web-automation/shared/node_modules` content that should be reviewed carefully as potentially noisy/accidental committed artifacts Co-authored-by: Stefano Fiorini <stefano.fiorini@firsthorizon.com> Reviewed-on: #1
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
// ⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
|
||
import process from "node:process";
|
||
import { pathToFileURL } from "node:url";
|
||
|
||
import { Command } from "commander";
|
||
|
||
import { resolveFormat } from "./command-helpers.js";
|
||
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, Writer } from "./types.js";
|
||
|
||
type CliContext = {
|
||
cwd?: string;
|
||
env?: NodeJS.ProcessEnv;
|
||
fetchImpl?: FetchLike;
|
||
stdout?: Writer;
|
||
stderr?: Writer;
|
||
};
|
||
|
||
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;
|
||
});
|
||
}
|