@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.
package/README.md CHANGED
@@ -1,166 +1,107 @@
1
1
  # zentao-mcp
2
2
 
3
- MCP server for ZenTao RESTful APIs (products + bugs).
4
-
5
- ## Quick Start
6
-
7
- ### Cursor IDE
8
-
9
- 1. Open Cursor Settings (⌘, on Mac or Ctrl+, on Windows/Linux)
10
- 2. Navigate to **Features** → **Model Context Protocol**
11
- 3. Click **Edit Config** to open `~/.cursor/mcp.json` (or create it)
12
- 4. Add the following configuration:
13
-
14
- ```json
15
- {
16
- "mcpServers": {
17
- "zentao-mcp": {
18
- "command": "npx",
19
- "args": [
20
- "-y",
21
- "@leeguoo/zentao-mcp",
22
- "--zentao-url=https://zentao.example.com/zentao",
23
- "--zentao-account=leo",
24
- "--zentao-password=***",
25
- "--stdio"
26
- ]
27
- }
28
- }
29
- }
30
- ```
3
+ ZenTao CLI for products + bugs.
4
+
5
+ ## Installation
31
6
 
32
- 5. Restart Cursor IDE
7
+ Global install:
33
8
 
34
- ### Other MCP Clients (Claude Desktop, etc.)
9
+ ```bash
10
+ npm i -g @leeguoo/zentao-mcp
11
+ ```
35
12
 
36
- For clients using TOML configuration (e.g., Claude Desktop), add to your MCP config file:
13
+ Or use without installing:
37
14
 
38
- ```toml
39
- [mcp_servers."zentao-mcp"]
40
- command = "npx"
41
- args = [
42
- "-y",
43
- "@leeguoo/zentao-mcp",
44
- "--zentao-url=https://zentao.example.com/zentao",
45
- "--zentao-account=leo",
46
- "--zentao-password=***",
47
- "--stdio"
48
- ]
15
+ ```bash
16
+ npx -y @leeguoo/zentao-mcp --help
49
17
  ```
50
18
 
51
- **Config file locations:**
52
- - Claude Desktop: `~/Library/Application Support/Claude/claude_desktop_config.toml` (Mac) or `%APPDATA%\Claude\claude_desktop_config.toml` (Windows)
53
- - Cursor: `~/.cursor/mcp.json` (JSON format)
19
+ This installs the `zentao` command (and keeps `zentao-mcp` as a compatibility alias).
54
20
 
55
21
  ## Configuration
56
22
 
57
23
  ### Required Parameters
58
24
 
59
- You can configure the server using CLI arguments or environment variables:
25
+ You can configure the CLI using CLI arguments or environment variables:
26
+
27
+ CLI arguments:
60
28
 
61
- **CLI Arguments:**
62
29
  - `--zentao-url` (e.g. `https://zentao.example.com/zentao`)
63
30
  - `--zentao-account`
64
31
  - `--zentao-password`
65
32
 
66
- **Environment Variables:**
67
- - `ZENTAO_URL` (e.g. `https://zentao.example.com/zentao`)
33
+ Environment variables:
34
+
35
+ - `ZENTAO_URL`
68
36
  - `ZENTAO_ACCOUNT`
69
37
  - `ZENTAO_PASSWORD`
70
38
 
71
- ### Using Environment Variables in Cursor
72
-
73
- If you prefer to use environment variables instead of CLI args, you can configure them in Cursor:
74
-
75
- ```json
76
- {
77
- "mcpServers": {
78
- "zentao-mcp": {
79
- "command": "npx",
80
- "args": ["-y", "@leeguoo/zentao-mcp", "--stdio"],
81
- "env": {
82
- "ZENTAO_URL": "https://zentao.example.com/zentao",
83
- "ZENTAO_ACCOUNT": "leo",
84
- "ZENTAO_PASSWORD": "***"
85
- }
86
- }
87
- }
88
- }
39
+ Tip: `ZENTAO_URL` should include the ZenTao base path (often `/zentao`).
40
+
41
+ ## Commands
42
+
43
+ List products:
44
+
45
+ ```bash
46
+ zentao products list
89
47
  ```
90
48
 
91
- **Tip:** `ZENTAO_URL` should include the ZenTao base path (often `/zentao`).
49
+ List bugs for a product:
92
50
 
93
- ## Tools
51
+ ```bash
52
+ zentao bugs list --product 1
53
+ ```
94
54
 
95
- The MCP server provides three tools that can be triggered by natural language in Cursor:
55
+ Get bug details:
96
56
 
97
- - **`zentao_products_list`** - List all products
98
- - **`zentao_bugs_list`** - List bugs for a specific product
99
- - **`zentao_bugs_mine`** - List my bugs by assignment or creator (status filter supported)
57
+ ```bash
58
+ zentao bug get --id 123
59
+ ```
100
60
 
101
- ### Usage Examples
61
+ List my bugs:
102
62
 
103
- After configuring the MCP server in Cursor, you can use natural language to interact with ZenTao:
63
+ ```bash
64
+ zentao bugs mine --scope assigned --status active
65
+ ```
104
66
 
105
- **English:**
106
- - "Show me all products"
107
- - "List bugs for product 1"
108
- - "Show me bugs"
109
- - "Show my bugs"
110
- - "List bugs assigned to me"
111
- - "View bugs in product 2"
67
+ Self test:
112
68
 
113
- **Chinese (中文):**
114
- - "看bug" / "查看bug" / "显示bug"
115
- - "产品1的bug列表"
116
- - "显示所有产品"
117
- - "查看产品2的问题"
118
- - "我的bug"
119
- - "分配给我的bug"
69
+ ```bash
70
+ zentao self-test
71
+ ```
120
72
 
121
- The AI will automatically:
122
- 1. Use `zentao_products_list` to get product IDs when needed
123
- 2. Use `zentao_bugs_list` when you ask to see bugs
124
- 3. Use `zentao_bugs_mine` when you ask for your own bugs
73
+ ## Login
125
74
 
126
- ### Tool Parameters
75
+ Save credentials locally (stored as plaintext TOML under your user config directory):
127
76
 
128
- **zentao_products_list:**
129
- ```json
130
- {
131
- "page": 1,
132
- "limit": 1000
133
- }
77
+ ```bash
78
+ zentao login --zentao-url=https://zentao.example.com/zentao --zentao-account=leo --zentao-password=***
134
79
  ```
135
80
 
136
- **zentao_bugs_list:**
137
- ```json
138
- {
139
- "product": 1,
140
- "page": 1,
141
- "limit": 20
142
- }
81
+ Config file:
82
+
83
+ - `~/.config/zentao/config.toml` (or `$XDG_CONFIG_HOME/zentao/config.toml`)
84
+
85
+ Then commands can omit auth flags:
86
+
87
+ ```bash
88
+ zentao whoami
89
+ zentao products list
143
90
  ```
144
91
 
145
- **zentao_bugs_mine:**
146
- ```json
147
- {
148
- "status": "active",
149
- "scope": "assigned",
150
- "includeZero": false,
151
- "includeDetails": true,
152
- "maxItems": 50
153
- }
92
+ ## Release (maintainers)
93
+
94
+ Requires `git`, `npm`, and `gh`.
95
+
96
+ ```bash
97
+ zentao release patch --dry-run
154
98
  ```
155
99
 
156
100
  ## Local Development
157
101
 
158
102
  ```bash
159
103
  pnpm install
160
- ZENTAO_URL=https://zentao.example.com/zentao \\
161
- ZENTAO_ACCOUNT=leo \\
162
- ZENTAO_PASSWORD=*** \\
163
- pnpm start
104
+ pnpm test
164
105
  ```
165
106
 
166
107
  ## Security
package/package.json CHANGED
@@ -1,14 +1,10 @@
1
1
  {
2
2
  "name": "@leeguoo/zentao-mcp",
3
- "version": "0.4.0",
4
- "description": "MCP server for ZenTao RESTful APIs",
3
+ "version": "0.5.0",
4
+ "description": "ZenTao CLI for products + bugs",
5
5
  "keywords": [
6
6
  "zentao",
7
7
  "chandao",
8
- "mcp",
9
- "modelcontextprotocol",
10
- "llm",
11
- "ai",
12
8
  "api",
13
9
  "rest",
14
10
  "bug-tracker",
@@ -20,10 +16,12 @@
20
16
  ],
21
17
  "type": "module",
22
18
  "bin": {
19
+ "zentao": "src/index.js",
23
20
  "zentao-mcp": "src/index.js"
24
21
  },
25
22
  "files": [
26
23
  "src",
24
+ "skills",
27
25
  "README.md"
28
26
  ],
29
27
  "publishConfig": {
@@ -31,13 +29,15 @@
31
29
  },
32
30
  "scripts": {
33
31
  "start": "node src/index.js",
34
- "self-test": "node scripts/self-test.mjs",
35
- "release": "./scripts/release.sh",
36
- "release:patch": "./scripts/release.sh patch",
37
- "release:minor": "./scripts/release.sh minor",
38
- "release:major": "./scripts/release.sh major"
32
+ "self-test": "node src/index.js self-test",
33
+ "release": "node src/index.js release",
34
+ "release:patch": "node src/index.js release patch",
35
+ "release:minor": "node src/index.js release minor",
36
+ "release:major": "node src/index.js release major",
37
+ "test": "node --test"
39
38
  },
40
- "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.25.1"
39
+ "dependencies": {},
40
+ "engines": {
41
+ "node": ">=18"
42
42
  }
43
43
  }
@@ -0,0 +1,106 @@
1
+ # zentao (ZenTao CLI)
2
+
3
+ This package provides a CLI for ZenTao REST API (products + bugs).
4
+
5
+ ## Installation
6
+
7
+ Global install:
8
+
9
+ ```bash
10
+ npm i -g @leeguoo/zentao-mcp
11
+ ```
12
+
13
+ Or use without installing:
14
+
15
+ ```bash
16
+ npx -y @leeguoo/zentao-mcp --help
17
+ ```
18
+
19
+ This installs the `zentao` command (and keeps `zentao-mcp` as a compatibility alias).
20
+
21
+ ## Authentication
22
+
23
+ You can pass credentials via environment variables:
24
+
25
+ ```bash
26
+ export ZENTAO_URL="https://zentao.example.com/zentao"
27
+ export ZENTAO_ACCOUNT="leo"
28
+ export ZENTAO_PASSWORD="***"
29
+ ```
30
+
31
+ Or via CLI flags:
32
+
33
+ - `--zentao-url`
34
+ - `--zentao-account`
35
+ - `--zentao-password`
36
+
37
+ ## Commands
38
+
39
+ ### List products
40
+
41
+ ```bash
42
+ zentao products list
43
+ ```
44
+
45
+ ### List bugs for a product
46
+
47
+ ```bash
48
+ zentao bugs list --product 1
49
+ ```
50
+
51
+ ### Get bug details
52
+
53
+ ```bash
54
+ zentao bug get --id 123
55
+ ```
56
+
57
+ ### List my bugs
58
+
59
+ ```bash
60
+ zentao bugs mine --scope assigned --status active
61
+ ```
62
+
63
+ Common options:
64
+
65
+ - `--scope`: `assigned|opened|resolved|all`
66
+ - `--status`: `active|resolved|closed|all` (supports `,` or `|` separated)
67
+ - `--include-details`: include bug list in response
68
+ - `--product-ids`: limit scan to specific products, e.g. `--product-ids=1,2,3`
69
+
70
+ ### Self test
71
+
72
+ ```bash
73
+ zentao self-test
74
+ ```
75
+
76
+ Use `--expected N` to make it CI-friendly (exit code 2 when mismatch):
77
+
78
+ ```bash
79
+ zentao self-test --expected 0
80
+ ```
81
+
82
+ ## Login / Whoami
83
+
84
+ Save credentials locally:
85
+
86
+ ```bash
87
+ zentao login --zentao-url=https://zentao.example.com/zentao --zentao-account=leo --zentao-password=***
88
+ ```
89
+
90
+ Config file:
91
+
92
+ - `~/.config/zentao/config.toml` (or `$XDG_CONFIG_HOME/zentao/config.toml`)
93
+
94
+ Then:
95
+
96
+ ```bash
97
+ zentao whoami
98
+ ```
99
+
100
+ ### Release (maintainers)
101
+
102
+ Requires `git`, `npm`, and `gh`.
103
+
104
+ ```bash
105
+ zentao release patch --dry-run
106
+ ```
@@ -0,0 +1,99 @@
1
+ export function parseCliArgs(argv) {
2
+ const args = {};
3
+ for (let i = 0; i < argv.length; i += 1) {
4
+ const raw = argv[i];
5
+ if (!raw.startsWith("--")) continue;
6
+ const [flag, inlineValue] = raw.split("=", 2);
7
+ const key = flag.replace(/^--/, "");
8
+ if (inlineValue !== undefined) {
9
+ args[key] = inlineValue;
10
+ continue;
11
+ }
12
+ const next = argv[i + 1];
13
+ if (next && !next.startsWith("--")) {
14
+ args[key] = next;
15
+ i += 1;
16
+ continue;
17
+ }
18
+ args[key] = true;
19
+ }
20
+ return args;
21
+ }
22
+
23
+ export function hasHelpFlag(argv) {
24
+ return argv.includes("--help") || argv.includes("-h");
25
+ }
26
+
27
+ export function getOption(cliArgs, env, envName, cliName) {
28
+ if (cliArgs[cliName]) return cliArgs[cliName];
29
+ const envValue = env[envName];
30
+ if (envValue) return envValue;
31
+ return null;
32
+ }
33
+
34
+ function isFlagToken(raw) {
35
+ return raw.startsWith("-") && raw !== "-";
36
+ }
37
+
38
+ export function splitSubcommand(argv) {
39
+ // Goal: find the first positional argument that isn't a flag or a flag value.
40
+ // This allows: `zentao-mcp self-test --foo=bar`.
41
+ // It also avoids treating `--zentao-url https://...` value as a subcommand.
42
+ for (let i = 0; i < argv.length; i += 1) {
43
+ const raw = argv[i];
44
+
45
+ if (raw === "--") {
46
+ return { command: null, commandArgv: argv };
47
+ }
48
+
49
+ if (raw.startsWith("--")) {
50
+ const hasInlineValue = raw.includes("=");
51
+ if (hasInlineValue) continue;
52
+ const next = argv[i + 1];
53
+ if (next && !isFlagToken(next)) {
54
+ i += 1;
55
+ }
56
+ continue;
57
+ }
58
+
59
+ if (raw === "-h") continue;
60
+ if (raw === "-v") continue;
61
+ if (raw.startsWith("-")) continue;
62
+
63
+ return { command: raw, commandArgv: argv.slice(i + 1) };
64
+ }
65
+
66
+ return { command: null, commandArgv: argv };
67
+ }
68
+
69
+ function findFirstPositional(argv) {
70
+ for (let i = 0; i < argv.length; i += 1) {
71
+ const raw = argv[i];
72
+ if (raw === "--") continue;
73
+
74
+ if (raw.startsWith("--")) {
75
+ const hasInlineValue = raw.includes("=");
76
+ if (hasInlineValue) continue;
77
+ const next = argv[i + 1];
78
+ if (next && !isFlagToken(next)) {
79
+ i += 1;
80
+ }
81
+ continue;
82
+ }
83
+
84
+ if (raw === "-h") continue;
85
+ if (raw === "-v") continue;
86
+ if (raw.startsWith("-")) continue;
87
+
88
+ return { index: i, value: raw };
89
+ }
90
+
91
+ return { index: -1, value: null };
92
+ }
93
+
94
+ export function extractCommand(argv) {
95
+ const { index, value } = findFirstPositional(argv);
96
+ if (!value) return { command: null, argv };
97
+ const nextArgv = argv.slice(0, index).concat(argv.slice(index + 1));
98
+ return { command: value, argv: nextArgv };
99
+ }
@@ -0,0 +1,49 @@
1
+ export function printRootHelp() {
2
+ // Keep this plain text: many users run via npx and paste output.
3
+ process.stdout.write(`zentao - ZenTao CLI\n\n`);
4
+ process.stdout.write(`Usage:\n`);
5
+ process.stdout.write(` zentao login [--zentao-url ... --zentao-account ... --zentao-password ...] [--yes]\n`);
6
+ process.stdout.write(` zentao whoami\n`);
7
+ process.stdout.write(` zentao products list [--page N] [--limit N]\n`);
8
+ process.stdout.write(` zentao bugs list --product <id> [--page N] [--limit N]\n`);
9
+ process.stdout.write(` zentao bug get --id <bugId>\n`);
10
+ process.stdout.write(` zentao bugs mine [--scope ...] [--status ...] [--include-details]\n`);
11
+ process.stdout.write(` zentao self-test [--expected N]\n`);
12
+ process.stdout.write(` zentao release [patch|minor|major] [--dry-run] [--yes]\n\n`);
13
+ process.stdout.write(`Auth options:\n`);
14
+ process.stdout.write(` --zentao-url or env ZENTAO_URL\n`);
15
+ process.stdout.write(` --zentao-account or env ZENTAO_ACCOUNT\n`);
16
+ process.stdout.write(` --zentao-password or env ZENTAO_PASSWORD\n`);
17
+ process.stdout.write(` --help, -h show help\n\n`);
18
+ process.stdout.write(`Subcommands:\n`);
19
+ process.stdout.write(` login save credentials locally\n`);
20
+ process.stdout.write(` whoami show current account\n`);
21
+ process.stdout.write(` products ZenTao products\n`);
22
+ process.stdout.write(` bugs ZenTao bugs\n`);
23
+ process.stdout.write(` bug ZenTao bug\n`);
24
+ process.stdout.write(` self-test run a basic API roundtrip\n`);
25
+ process.stdout.write(` release version bump + tag + gh release + npm publish\n`);
26
+ }
27
+
28
+ export function printSelfTestHelp() {
29
+ process.stdout.write(`zentao self-test - verify API access\n\n`);
30
+ process.stdout.write(`Usage:\n`);
31
+ process.stdout.write(` zentao self-test --zentao-url=... --zentao-account=... --zentao-password=...\n`);
32
+ process.stdout.write(` ZENTAO_URL=... ZENTAO_ACCOUNT=... ZENTAO_PASSWORD=... zentao self-test\n\n`);
33
+ process.stdout.write(`Options:\n`);
34
+ process.stdout.write(` --expected N exit 2 if total != N\n`);
35
+ process.stdout.write(` --help, -h show help\n`);
36
+ }
37
+
38
+ export function printReleaseHelp() {
39
+ process.stdout.write(`zentao release - create a new release\n\n`);
40
+ process.stdout.write(`Usage:\n`);
41
+ process.stdout.write(` zentao release [patch|minor|major] [--dry-run] [--yes]\n\n`);
42
+ process.stdout.write(`Options:\n`);
43
+ process.stdout.write(` --dry-run print planned commands, do not run\n`);
44
+ process.stdout.write(` --yes auto-confirm prompts (dirty git, npm login)\n`);
45
+ process.stdout.write(` --skip-push do not push commits/tags\n`);
46
+ process.stdout.write(` --skip-github-release do not create GitHub release\n`);
47
+ process.stdout.write(` --skip-publish do not npm publish\n`);
48
+ process.stdout.write(` --help, -h show help\n`);
49
+ }
@@ -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 bug get\n\n`);
7
+ process.stdout.write(`Usage:\n`);
8
+ process.stdout.write(` zentao-mcp bug get --id <bugId>\n`);
9
+ }
10
+
11
+ export async function runBug({ 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 !== "get") throw new Error(`Unknown bug subcommand: ${sub || "(missing)"}`);
19
+
20
+ const cliArgs = parseCliArgs(argvWithoutSub);
21
+ const id = cliArgs.id;
22
+ if (!id) throw new Error("Missing --id");
23
+
24
+ const api = createClientFromCli({ argv: argvWithoutSub, env });
25
+ const result = await api.getBug({ id });
26
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
27
+ }
@@ -0,0 +1,68 @@
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-mcp bugs <subcommand>\n\n`);
22
+ process.stdout.write(`Usage:\n`);
23
+ process.stdout.write(` zentao-mcp bugs list --product <id> [--page N] [--limit N]\n`);
24
+ process.stdout.write(` zentao-mcp bugs mine [--scope assigned|opened|resolved|all] [--status active|resolved|closed|all] [--include-details]\n`);
25
+ }
26
+
27
+ export async function runBugs({ argv = [], env = process.env } = {}) {
28
+ if (hasHelpFlag(argv)) {
29
+ printHelp();
30
+ return;
31
+ }
32
+
33
+ const { command: sub, argv: argvWithoutSub } = extractCommand(argv);
34
+ const cliArgs = parseCliArgs(argvWithoutSub);
35
+ const api = createClientFromCli({ argv: argvWithoutSub, env });
36
+
37
+ if (sub === "list") {
38
+ const product = cliArgs.product;
39
+ if (!product) throw new Error("Missing --product");
40
+ const result = await api.listBugs({
41
+ product,
42
+ page: cliArgs.page,
43
+ limit: cliArgs.limit,
44
+ });
45
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
46
+ return;
47
+ }
48
+
49
+ if (sub === "mine") {
50
+ const includeDetails = Boolean(cliArgs["include-details"]);
51
+ const includeZero = Boolean(cliArgs["include-zero"]);
52
+ const productIds = parseCsvIntegers(cliArgs["product-ids"]);
53
+ const result = await api.bugsMine({
54
+ account: cliArgs.account,
55
+ scope: cliArgs.scope,
56
+ status: cliArgs.status,
57
+ productIds,
58
+ includeZero,
59
+ perPage: cliArgs["per-page"],
60
+ maxItems: cliArgs["max-items"],
61
+ includeDetails,
62
+ });
63
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
64
+ return;
65
+ }
66
+
67
+ throw new Error(`Unknown bugs subcommand: ${sub || "(missing)"}`);
68
+ }
@@ -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
+ }