@leeguoo/zentao-mcp 0.4.1 → 0.5.1
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/README.md +93 -139
- package/package.json +13 -13
- package/skills/zentao-cli.md +130 -0
- package/src/cli/args.js +99 -0
- package/src/cli/help.js +49 -0
- package/src/commands/bug.js +69 -0
- package/src/commands/bugs.js +138 -0
- package/src/commands/login.js +60 -0
- package/src/commands/products.js +58 -0
- package/src/commands/release.js +198 -0
- package/src/commands/selftest.js +52 -0
- package/src/commands/whoami.js +32 -0
- package/src/config/store.js +81 -0
- package/src/index.js +49 -453
- package/src/zentao/client.js +324 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { extractCommand, hasHelpFlag, parseCliArgs } from "../cli/args.js";
|
|
3
|
+
import { createClientFromCli } from "../zentao/client.js";
|
|
4
|
+
|
|
5
|
+
function parseCsvIntegers(value) {
|
|
6
|
+
if (value === undefined || value === null || value === "") return null;
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
const nested = value.flatMap((item) => String(item).split(/[,|]/));
|
|
9
|
+
const parsed = nested.map((item) => Number(item)).filter((n) => Number.isFinite(n));
|
|
10
|
+
return parsed.length ? parsed : null;
|
|
11
|
+
}
|
|
12
|
+
const tokens = String(value)
|
|
13
|
+
.split(/[,|]/)
|
|
14
|
+
.map((item) => item.trim())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
const parsed = tokens.map((item) => Number(item)).filter((n) => Number.isFinite(n));
|
|
17
|
+
return parsed.length ? parsed : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printHelp() {
|
|
21
|
+
process.stdout.write(`zentao bugs <subcommand>\n\n`);
|
|
22
|
+
process.stdout.write(`Usage:\n`);
|
|
23
|
+
process.stdout.write(` zentao bugs list --product <id> [--page N] [--limit N] [--json]\n`);
|
|
24
|
+
process.stdout.write(` zentao bugs mine [--scope assigned|opened|resolved|all] [--status active|resolved|closed|all] [--include-details] [--json]\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatAccount(value) {
|
|
28
|
+
if (!value) return "";
|
|
29
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
30
|
+
if (typeof value === "object") return String(value.account || value.name || value.realname || "");
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatBugsSimple(bugs) {
|
|
35
|
+
const rows = [];
|
|
36
|
+
rows.push(["id", "title", "status", "pri", "severity", "assignedTo"].join("\t"));
|
|
37
|
+
for (const bug of bugs) {
|
|
38
|
+
rows.push(
|
|
39
|
+
[
|
|
40
|
+
String(bug?.id ?? ""),
|
|
41
|
+
String(bug?.title ?? ""),
|
|
42
|
+
String(bug?.status ?? ""),
|
|
43
|
+
String(bug?.pri ?? ""),
|
|
44
|
+
String(bug?.severity ?? ""),
|
|
45
|
+
formatAccount(bug?.assignedTo),
|
|
46
|
+
].join("\t")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return `${rows.join("\n")}\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function formatBugsMineSimple(result) {
|
|
53
|
+
const total = result?.total ?? 0;
|
|
54
|
+
const products = Array.isArray(result?.products) ? result.products : [];
|
|
55
|
+
const rows = [];
|
|
56
|
+
rows.push(`total\t${total}`);
|
|
57
|
+
rows.push(["id", "name", "myBugs", "totalBugs"].join("\t"));
|
|
58
|
+
for (const product of products) {
|
|
59
|
+
rows.push(
|
|
60
|
+
[
|
|
61
|
+
String(product?.id ?? ""),
|
|
62
|
+
String(product?.name ?? ""),
|
|
63
|
+
String(product?.myBugs ?? ""),
|
|
64
|
+
String(product?.totalBugs ?? ""),
|
|
65
|
+
].join("\t")
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bugs = Array.isArray(result?.bugs) ? result.bugs : [];
|
|
70
|
+
if (bugs.length) {
|
|
71
|
+
rows.push("");
|
|
72
|
+
rows.push("bugs");
|
|
73
|
+
rows.push(formatBugsSimple(bugs).trimEnd());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `${rows.join("\n")}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runBugs({ argv = [], env = process.env } = {}) {
|
|
80
|
+
if (hasHelpFlag(argv)) {
|
|
81
|
+
printHelp();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { command: sub, argv: argvWithoutSub } = extractCommand(argv);
|
|
86
|
+
const cliArgs = parseCliArgs(argvWithoutSub);
|
|
87
|
+
const api = createClientFromCli({ argv: argvWithoutSub, env });
|
|
88
|
+
|
|
89
|
+
if (sub === "list") {
|
|
90
|
+
const product = cliArgs.product;
|
|
91
|
+
if (!product) throw new Error("Missing --product");
|
|
92
|
+
const result = await api.listBugs({
|
|
93
|
+
product,
|
|
94
|
+
page: cliArgs.page,
|
|
95
|
+
limit: cliArgs.limit,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (cliArgs.json) {
|
|
99
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const bugs = result?.result?.bugs;
|
|
104
|
+
if (!Array.isArray(bugs)) {
|
|
105
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.stdout.write(formatBugsSimple(bugs));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sub === "mine") {
|
|
114
|
+
const includeDetails = Boolean(cliArgs["include-details"]);
|
|
115
|
+
const includeZero = Boolean(cliArgs["include-zero"]);
|
|
116
|
+
const productIds = parseCsvIntegers(cliArgs["product-ids"]);
|
|
117
|
+
const result = await api.bugsMine({
|
|
118
|
+
account: cliArgs.account,
|
|
119
|
+
scope: cliArgs.scope,
|
|
120
|
+
status: cliArgs.status,
|
|
121
|
+
productIds,
|
|
122
|
+
includeZero,
|
|
123
|
+
perPage: cliArgs["per-page"],
|
|
124
|
+
maxItems: cliArgs["max-items"],
|
|
125
|
+
includeDetails,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (cliArgs.json) {
|
|
129
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.stdout.write(formatBugsMineSimple(result?.result));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(`Unknown bugs subcommand: ${sub || "(missing)"}`);
|
|
138
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { hasHelpFlag, parseCliArgs, getOption } from "../cli/args.js";
|
|
4
|
+
import { saveConfig } from "../config/store.js";
|
|
5
|
+
import { ZentaoClient } from "../zentao/client.js";
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
process.stdout.write(`zentao login - save credentials locally\n\n`);
|
|
9
|
+
process.stdout.write(`Usage:\n`);
|
|
10
|
+
process.stdout.write(` zentao login --zentao-url=... --zentao-account=... --zentao-password=... [--yes]\n`);
|
|
11
|
+
process.stdout.write(`\n`);
|
|
12
|
+
process.stdout.write(`Notes:\n`);
|
|
13
|
+
process.stdout.write(` Credentials are stored as plaintext TOML in your user config directory.\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function prompt(question) {
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
try {
|
|
19
|
+
return await new Promise((resolve) => rl.question(question, resolve));
|
|
20
|
+
} finally {
|
|
21
|
+
rl.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runLogin({ argv = [], env = process.env } = {}) {
|
|
26
|
+
if (hasHelpFlag(argv)) {
|
|
27
|
+
printHelp();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const cliArgs = parseCliArgs(argv);
|
|
32
|
+
const yes = Boolean(cliArgs.yes);
|
|
33
|
+
let baseUrl = getOption(cliArgs, env, "ZENTAO_URL", "zentao-url");
|
|
34
|
+
let account = getOption(cliArgs, env, "ZENTAO_ACCOUNT", "zentao-account");
|
|
35
|
+
let password = getOption(cliArgs, env, "ZENTAO_PASSWORD", "zentao-password");
|
|
36
|
+
|
|
37
|
+
if (!baseUrl && !yes) baseUrl = String(await prompt("ZENTAO_URL: ")).trim();
|
|
38
|
+
if (!account && !yes) account = String(await prompt("ZENTAO_ACCOUNT: ")).trim();
|
|
39
|
+
if (!password && !yes) password = String(await prompt("ZENTAO_PASSWORD (echoed): ")).trim();
|
|
40
|
+
|
|
41
|
+
if (!baseUrl || !account || !password) {
|
|
42
|
+
throw new Error("Missing credentials. Provide flags/env, or run interactively.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Verify credentials by requesting a token.
|
|
46
|
+
const client = new ZentaoClient({ baseUrl, account, password });
|
|
47
|
+
await client.ensureToken();
|
|
48
|
+
|
|
49
|
+
const filePath = saveConfig(
|
|
50
|
+
{
|
|
51
|
+
zentaoUrl: baseUrl,
|
|
52
|
+
zentaoAccount: account,
|
|
53
|
+
zentaoPassword: password,
|
|
54
|
+
},
|
|
55
|
+
{ env }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
process.stdout.write(`Logged in as ${account}\n`);
|
|
59
|
+
process.stdout.write(`Saved to ${filePath}\n`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { extractCommand, hasHelpFlag, parseCliArgs } from "../cli/args.js";
|
|
3
|
+
import { createClientFromCli } from "../zentao/client.js";
|
|
4
|
+
|
|
5
|
+
function printHelp() {
|
|
6
|
+
process.stdout.write(`zentao products list\n\n`);
|
|
7
|
+
process.stdout.write(`Usage:\n`);
|
|
8
|
+
process.stdout.write(` zentao products list [--page N] [--limit N] [--json]\n`);
|
|
9
|
+
process.stdout.write(`\n`);
|
|
10
|
+
process.stdout.write(`Options:\n`);
|
|
11
|
+
process.stdout.write(` --json print full JSON payload\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatProductsSimple(products) {
|
|
15
|
+
const rows = [];
|
|
16
|
+
rows.push(["id", "name", "totalBugs", "status"].join("\t"));
|
|
17
|
+
for (const product of products) {
|
|
18
|
+
rows.push(
|
|
19
|
+
[
|
|
20
|
+
String(product.id ?? ""),
|
|
21
|
+
String(product.name ?? ""),
|
|
22
|
+
String(product.totalBugs ?? product.totalBugsCount ?? ""),
|
|
23
|
+
String(product.status ?? product.productStatus ?? ""),
|
|
24
|
+
].join("\t")
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return `${rows.join("\n")}\n`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runProducts({ argv = [], env = process.env } = {}) {
|
|
31
|
+
if (hasHelpFlag(argv)) {
|
|
32
|
+
printHelp();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { command: sub, argv: argvWithoutSub } = extractCommand(argv);
|
|
37
|
+
if (sub !== "list") throw new Error(`Unknown products subcommand: ${sub || "(missing)"}`);
|
|
38
|
+
|
|
39
|
+
const cliArgs = parseCliArgs(argvWithoutSub);
|
|
40
|
+
const api = createClientFromCli({ argv: argvWithoutSub, env });
|
|
41
|
+
const result = await api.listProducts({
|
|
42
|
+
page: cliArgs.page,
|
|
43
|
+
limit: cliArgs.limit,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (cliArgs.json) {
|
|
47
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const products = result?.result?.products;
|
|
52
|
+
if (!Array.isArray(products)) {
|
|
53
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.stdout.write(formatProductsSimple(products));
|
|
58
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { extractCommand, hasHelpFlag, parseCliArgs } from "../cli/args.js";
|
|
7
|
+
import { printReleaseHelp } from "../cli/help.js";
|
|
8
|
+
|
|
9
|
+
function run(cmd, args, { cwd, env, dryRun } = {}) {
|
|
10
|
+
const printable = [cmd, ...args].join(" ");
|
|
11
|
+
if (dryRun) {
|
|
12
|
+
process.stdout.write(`[dry-run] ${printable}\n`);
|
|
13
|
+
return { status: 0 };
|
|
14
|
+
}
|
|
15
|
+
const res = spawnSync(cmd, args, {
|
|
16
|
+
cwd,
|
|
17
|
+
env,
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
});
|
|
20
|
+
if (res.status !== 0) {
|
|
21
|
+
const error = new Error(`Command failed (${res.status}): ${printable}`);
|
|
22
|
+
error.exitCode = res.status || 1;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function commandExists(name) {
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
const res = spawnSync("where", [name], { stdio: "ignore" });
|
|
31
|
+
return res.status === 0;
|
|
32
|
+
}
|
|
33
|
+
const res = spawnSync("sh", ["-lc", `command -v ${name}`], {
|
|
34
|
+
stdio: "ignore",
|
|
35
|
+
});
|
|
36
|
+
return res.status === 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function confirm(question, { yes }) {
|
|
40
|
+
if (yes) return true;
|
|
41
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
42
|
+
try {
|
|
43
|
+
const answer = await new Promise((resolve) => {
|
|
44
|
+
rl.question(`${question} (y/n) `, (value) => resolve(value));
|
|
45
|
+
});
|
|
46
|
+
return String(answer).trim().toLowerCase().startsWith("y");
|
|
47
|
+
} finally {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readPackageVersion(repoRoot) {
|
|
53
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
55
|
+
return String(pkg.version || "0.0.0");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractChangelogNotes(repoRoot, version) {
|
|
59
|
+
const changelogPath = path.join(repoRoot, "CHANGELOG.md");
|
|
60
|
+
if (!fs.existsSync(changelogPath)) return `Release v${version}`;
|
|
61
|
+
const text = fs.readFileSync(changelogPath, "utf8");
|
|
62
|
+
const start = text.match(new RegExp(`^## \\s*\\[?${version.replace(/\./g, "\\.")}\\]?\\s*$`, "m"));
|
|
63
|
+
if (!start) return `Release v${version}`;
|
|
64
|
+
const startIndex = start.index ?? 0;
|
|
65
|
+
const rest = text.slice(startIndex);
|
|
66
|
+
const next = rest.slice(1).match(/^##\s/m);
|
|
67
|
+
const endIndex = next ? startIndex + 1 + (next.index ?? 0) : text.length;
|
|
68
|
+
return text.slice(startIndex, endIndex).trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function gitHasUncommittedChanges(repoRoot) {
|
|
72
|
+
const res = spawnSync("git", ["diff-index", "--quiet", "HEAD", "--"], {
|
|
73
|
+
cwd: repoRoot,
|
|
74
|
+
stdio: "ignore",
|
|
75
|
+
});
|
|
76
|
+
return res.status !== 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ensureGitRepo(repoRoot) {
|
|
80
|
+
const res = spawnSync("git", ["rev-parse", "--git-dir"], { cwd: repoRoot, stdio: "ignore" });
|
|
81
|
+
if (res.status !== 0) {
|
|
82
|
+
throw new Error("Not a git repo. Please run this command from a git repository.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ensureDependencies() {
|
|
87
|
+
const missing = [];
|
|
88
|
+
for (const cmd of ["git", "npm", "gh"]) {
|
|
89
|
+
if (!commandExists(cmd)) missing.push(cmd);
|
|
90
|
+
}
|
|
91
|
+
if (missing.length) {
|
|
92
|
+
throw new Error(`Missing required tools: ${missing.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runRelease({ argv = [], env = process.env } = {}) {
|
|
97
|
+
if (hasHelpFlag(argv)) {
|
|
98
|
+
printReleaseHelp();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { command: versionTypeRaw, argv: argvWithoutVersionType } = extractCommand(argv);
|
|
103
|
+
const versionType = versionTypeRaw || "patch";
|
|
104
|
+
const cliArgs = parseCliArgs(argvWithoutVersionType);
|
|
105
|
+
const repoRoot = process.cwd();
|
|
106
|
+
|
|
107
|
+
const dryRun = Boolean(cliArgs["dry-run"]);
|
|
108
|
+
const yes = Boolean(cliArgs.yes);
|
|
109
|
+
const skipPush = Boolean(cliArgs["skip-push"]);
|
|
110
|
+
const skipGithubRelease = Boolean(cliArgs["skip-github-release"]);
|
|
111
|
+
const skipPublish = Boolean(cliArgs["skip-publish"]);
|
|
112
|
+
|
|
113
|
+
if (!new Set(["patch", "minor", "major"]).has(versionType)) {
|
|
114
|
+
throw new Error(`Invalid version type: ${versionType}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ensureDependencies();
|
|
118
|
+
ensureGitRepo(repoRoot);
|
|
119
|
+
|
|
120
|
+
if (gitHasUncommittedChanges(repoRoot)) {
|
|
121
|
+
const ok = await confirm("Warning: uncommitted changes. Continue?", { yes });
|
|
122
|
+
if (!ok) return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const currentVersion = readPackageVersion(repoRoot);
|
|
126
|
+
process.stdout.write(`Current version: ${currentVersion}\n`);
|
|
127
|
+
run("npm", ["version", versionType, "--no-git-tag-version"], { cwd: repoRoot, env, dryRun });
|
|
128
|
+
const newVersion = readPackageVersion(repoRoot);
|
|
129
|
+
process.stdout.write(`New version: ${newVersion}\n`);
|
|
130
|
+
|
|
131
|
+
run("git", ["add", "package.json"], { cwd: repoRoot, env, dryRun });
|
|
132
|
+
try {
|
|
133
|
+
run("git", ["commit", "-m", `chore: bump version to ${newVersion}`], { cwd: repoRoot, env, dryRun });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (!/nothing to commit/i.test(String(error.message || ""))) throw error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
run("git", ["tag", "-a", `v${newVersion}`, "-m", `Release v${newVersion}`], {
|
|
139
|
+
cwd: repoRoot,
|
|
140
|
+
env,
|
|
141
|
+
dryRun,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!skipPush) {
|
|
145
|
+
const pushAttempts = [
|
|
146
|
+
["push", "origin", "main"],
|
|
147
|
+
["push", "origin", "master"],
|
|
148
|
+
["push"],
|
|
149
|
+
];
|
|
150
|
+
let pushed = false;
|
|
151
|
+
for (const args of pushAttempts) {
|
|
152
|
+
try {
|
|
153
|
+
run("git", args, { cwd: repoRoot, env, dryRun });
|
|
154
|
+
pushed = true;
|
|
155
|
+
break;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (dryRun) {
|
|
158
|
+
pushed = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!pushed && !dryRun) {
|
|
164
|
+
throw new Error("Failed to push branch.");
|
|
165
|
+
}
|
|
166
|
+
run("git", ["push", "origin", `v${newVersion}`], { cwd: repoRoot, env, dryRun });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!skipGithubRelease) {
|
|
170
|
+
const notes = extractChangelogNotes(repoRoot, newVersion);
|
|
171
|
+
run(
|
|
172
|
+
"gh",
|
|
173
|
+
[
|
|
174
|
+
"release",
|
|
175
|
+
"create",
|
|
176
|
+
`v${newVersion}`,
|
|
177
|
+
"--title",
|
|
178
|
+
`v${newVersion}`,
|
|
179
|
+
"--notes",
|
|
180
|
+
notes,
|
|
181
|
+
"--latest",
|
|
182
|
+
],
|
|
183
|
+
{ cwd: repoRoot, env, dryRun }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!skipPublish) {
|
|
188
|
+
const whoami = spawnSync("npm", ["whoami"], { cwd: repoRoot, stdio: "ignore" });
|
|
189
|
+
if (whoami.status !== 0) {
|
|
190
|
+
const ok = await confirm("Warning: not logged into npm. Run npm login now?", { yes });
|
|
191
|
+
if (!ok) return;
|
|
192
|
+
run("npm", ["login"], { cwd: repoRoot, env, dryRun });
|
|
193
|
+
}
|
|
194
|
+
run("npm", ["publish", "--access", "public"], { cwd: repoRoot, env, dryRun });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.stdout.write(`Release done: v${newVersion}\n`);
|
|
198
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { getOption, hasHelpFlag, parseCliArgs } from "../cli/args.js";
|
|
3
|
+
import { printSelfTestHelp } from "../cli/help.js";
|
|
4
|
+
import { createClientFromCli } from "../zentao/client.js";
|
|
5
|
+
|
|
6
|
+
export async function runSelfTest({ argv = [], env = process.env } = {}) {
|
|
7
|
+
if (hasHelpFlag(argv)) {
|
|
8
|
+
printSelfTestHelp();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const cliArgs = parseCliArgs(argv);
|
|
13
|
+
const baseUrl = getOption(cliArgs, env, "ZENTAO_URL", "zentao-url");
|
|
14
|
+
const account = getOption(cliArgs, env, "ZENTAO_ACCOUNT", "zentao-account");
|
|
15
|
+
const password = getOption(cliArgs, env, "ZENTAO_PASSWORD", "zentao-password");
|
|
16
|
+
const expectedRaw = cliArgs.expected ?? null;
|
|
17
|
+
const expected = expectedRaw === null ? null : Number(expectedRaw);
|
|
18
|
+
|
|
19
|
+
if (!baseUrl || !account || !password) {
|
|
20
|
+
throw new Error("Missing ZENTAO_URL/ZENTAO_ACCOUNT/ZENTAO_PASSWORD (or CLI args). ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const api = createClientFromCli({ argv, env });
|
|
24
|
+
const result = await api.bugsMine({
|
|
25
|
+
scope: "assigned",
|
|
26
|
+
status: "active",
|
|
27
|
+
includeDetails: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.status !== 1) {
|
|
31
|
+
const error = new Error(`API error: ${JSON.stringify(result)}`);
|
|
32
|
+
error.exitCode = 1;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const total = result.result?.total ?? 0;
|
|
37
|
+
process.stdout.write(`assigned active bugs: ${total}\n`);
|
|
38
|
+
|
|
39
|
+
const products = result.result?.products ?? [];
|
|
40
|
+
if (Array.isArray(products) && products.length) {
|
|
41
|
+
const summary = products.map((item) => `${item.name}(${item.myBugs})`).join(", ");
|
|
42
|
+
process.stdout.write(`products: ${summary}\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Number.isFinite(expected) && expected !== null) {
|
|
46
|
+
if (total !== expected) {
|
|
47
|
+
const error = new Error(`Expected ${expected}, got ${total}.`);
|
|
48
|
+
error.exitCode = 2;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { hasHelpFlag } from "../cli/args.js";
|
|
3
|
+
import { loadConfig } from "../config/store.js";
|
|
4
|
+
import { ZentaoClient, createClientFromCli } from "../zentao/client.js";
|
|
5
|
+
|
|
6
|
+
function printHelp() {
|
|
7
|
+
process.stdout.write(`zentao whoami - show current account\n\n`);
|
|
8
|
+
process.stdout.write(`Usage:\n`);
|
|
9
|
+
process.stdout.write(` zentao whoami\n`);
|
|
10
|
+
process.stdout.write(`\n`);
|
|
11
|
+
process.stdout.write(`Reads credentials from flags/env, falling back to saved login config.\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function runWhoami({ argv = [], env = process.env } = {}) {
|
|
15
|
+
if (hasHelpFlag(argv)) {
|
|
16
|
+
printHelp();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const stored = loadConfig({ env }) || null;
|
|
21
|
+
const client = createClientFromCli({ argv, env });
|
|
22
|
+
|
|
23
|
+
// Best-effort verification: ensure token can be fetched.
|
|
24
|
+
await client.ensureToken();
|
|
25
|
+
|
|
26
|
+
const account = client.account;
|
|
27
|
+
const baseUrl = client.baseUrl;
|
|
28
|
+
process.stdout.write(`${account}\n`);
|
|
29
|
+
if (stored && (stored.zentaoUrl || stored.zentaoAccount)) {
|
|
30
|
+
process.stdout.write(`url: ${baseUrl}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function escapeTomlString(value) {
|
|
6
|
+
return String(value).replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\"/g, "\\\"");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function unescapeTomlString(value) {
|
|
10
|
+
return String(value)
|
|
11
|
+
.replace(/\\n/g, "\n")
|
|
12
|
+
.replace(/\\\"/g, '"')
|
|
13
|
+
.replace(/\\\\/g, "\\");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseToml(text) {
|
|
17
|
+
const out = {};
|
|
18
|
+
const lines = String(text).split(/\r?\n/);
|
|
19
|
+
for (const rawLine of lines) {
|
|
20
|
+
const line = rawLine.trim();
|
|
21
|
+
if (!line) continue;
|
|
22
|
+
if (line.startsWith("#")) continue;
|
|
23
|
+
if (line.startsWith("[")) continue;
|
|
24
|
+
|
|
25
|
+
const idx = line.indexOf("=");
|
|
26
|
+
if (idx === -1) continue;
|
|
27
|
+
|
|
28
|
+
const key = line.slice(0, idx).trim();
|
|
29
|
+
let value = line.slice(idx + 1).trim();
|
|
30
|
+
|
|
31
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
32
|
+
value = unescapeTomlString(value.slice(1, -1));
|
|
33
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
34
|
+
value = value.slice(1, -1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (key) out[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toToml(config) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
lines.push('# zentao CLI config');
|
|
45
|
+
lines.push('# WARNING: stored as plaintext');
|
|
46
|
+
if (config.zentaoUrl) lines.push(`zentaoUrl = "${escapeTomlString(config.zentaoUrl)}"`);
|
|
47
|
+
if (config.zentaoAccount) lines.push(`zentaoAccount = "${escapeTomlString(config.zentaoAccount)}"`);
|
|
48
|
+
if (config.zentaoPassword) lines.push(`zentaoPassword = "${escapeTomlString(config.zentaoPassword)}"`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getConfigDir(env) {
|
|
54
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
55
|
+
if (xdg) return xdg;
|
|
56
|
+
const home = os.homedir();
|
|
57
|
+
return path.join(home, ".config");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getConfigPath({ env = process.env } = {}) {
|
|
61
|
+
return path.join(getConfigDir(env), "zentao", "config.toml");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function loadConfig({ env = process.env } = {}) {
|
|
65
|
+
const filePath = getConfigPath({ env });
|
|
66
|
+
try {
|
|
67
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
68
|
+
const data = parseToml(raw);
|
|
69
|
+
if (!data || typeof data !== "object") return null;
|
|
70
|
+
return data;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function saveConfig(config, { env = process.env } = {}) {
|
|
77
|
+
const filePath = getConfigPath({ env });
|
|
78
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(filePath, toToml(config), "utf8");
|
|
80
|
+
return filePath;
|
|
81
|
+
}
|