@leeguoo/zentao-mcp 0.4.0 → 0.5.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.
@@ -0,0 +1,27 @@
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-mcp products list\n\n`);
7
+ process.stdout.write(`Usage:\n`);
8
+ process.stdout.write(` zentao-mcp products list [--page N] [--limit N] [--json]\n`);
9
+ }
10
+
11
+ export async function runProducts({ argv = [], env = process.env } = {}) {
12
+ if (hasHelpFlag(argv)) {
13
+ printHelp();
14
+ return;
15
+ }
16
+
17
+ const { command: sub, argv: argvWithoutSub } = extractCommand(argv);
18
+ if (sub !== "list") throw new Error(`Unknown products subcommand: ${sub || "(missing)"}`);
19
+
20
+ const cliArgs = parseCliArgs(argvWithoutSub);
21
+ const api = createClientFromCli({ argv: argvWithoutSub, env });
22
+ const result = await api.listProducts({
23
+ page: cliArgs.page,
24
+ limit: cliArgs.limit,
25
+ });
26
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
27
+ }
@@ -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
+ }