@ts-org/jenkins-cli 2.0.0 → 3.0.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 CHANGED
@@ -1,82 +1,199 @@
1
- ## @ts-org/jenkins-cli
1
+ ## Jenkins CLI
2
2
 
3
- Jenkins 部署命令行工具,支持交互式选择分支和环境。
3
+ 一个轻量的 Jenkins 部署工具,让你在命令行中通过交互式或非交互式的方式,轻松触发 Jenkins 参数化构建。
4
4
 
5
- ### 安装
5
+ ### ✨ 特性
6
+
7
+ - 交互式 CLI,引导你选择分支和部署环境。
8
+ - 智能触发,自动停止进行中的构建,并复用队列中相同的任务。
9
+ - 灵活的参数化构建,支持在运行时传入额外参数。
10
+ - 丰富的命令集,覆盖 Job、Builds、Queue 等常用操作。
11
+ - 支持 `jenkins-cli.yaml` 或 `package.json` 进行项目级配置。
12
+
13
+ ### 📦 安装
14
+
15
+ ```bash
16
+ # Node.js >= 16
17
+ npm install -g @ts-org/jenkins-cli
18
+
19
+ # Node.js >= 18,推荐使用pnpm
20
+ pnpm install -g @ts-org/jenkins-cli
21
+ ```
22
+
23
+ ### 🚀 使用
24
+
25
+ 在你的项目根目录下执行:
6
26
 
7
27
  ```bash
8
- npm add @ts-org/jenkins-cli
28
+ jenkins-cli
9
29
  ```
10
30
 
11
- ### 配置
31
+ CLI 会自动加载配置,列出 Git 分支和部署环境供你选择。
32
+
33
+ ![Demo](docs/images/demo.png)
34
+
35
+ ### ⚙️ 配置
12
36
 
13
37
  在项目根目录创建 `jenkins-cli.yaml`:
14
38
 
15
39
  ```yaml
16
- # Jenkins API Token 格式:http://username:token@host:port
17
- apiToken: http://username:token@jenkins.example.com:8080
40
+ # Jenkins API 地址,格式: http(s)://username:token@host:port
41
+ apiToken: http://user:token@jenkins.example.com
18
42
 
19
- # Jenkins Job 名称
20
- job: your-job-name
43
+ # Jenkins Job 名称 (可被 -j 参数覆盖)
44
+ job: your-project-job
21
45
 
22
- # 部署环境列表
46
+ # 部署环境列表 (将作为参数 `mode` 传入 Jenkins)
23
47
  modes:
24
48
  - dev
25
49
  - sit
26
50
  - uat
27
51
  ```
28
52
 
29
- 或者在 `package.json` 里配置:
53
+ #### 扩展交互参数
54
+
55
+ 通过 `configs` 字段,你可以添加自定义的交互式提问,其结果将作为参数传递给 Jenkins。
56
+
57
+ - `type`: 支持 `input`, `list`, `confirm` 等 [Inquirer.js](https://github.com/SBoudrias/Inquirer.js/) 类型。
58
+ - `choices`, `default`, `validate`: 支持从本地 TS/JS 文件动态加载。
59
+ - `path:Fun:funcName`:调用 `funcName()` 函数获取值 (支持 async)。
60
+ - `path:Var:varName`:获取 `varName` 变量的值 (支持 Promise)。
61
+ - `name`: 参数名,注意不要使用 `branch`, `mode`, `modes` 等保留字。
62
+
63
+ **示例:**
64
+
65
+ ```yaml
66
+ configs:
67
+ - type: input
68
+ name: version
69
+ message: '请输入版本号:'
70
+ default: '1.0.0'
71
+ - type: list
72
+ name: service
73
+ message: '请选择服务:'
74
+ choices: src/config.ts:Fun:getServices
75
+ - type: input
76
+ name: ticket
77
+ message: '请输入关联的工单号:'
78
+ validate: src/config.ts:Fun:validateTicket
79
+ ```
80
+
81
+ 对应的 `src/config.ts`:
82
+ ```ts
83
+ export async function getServices() {
84
+ return ['user-center', 'order-service'];
85
+ }
30
86
 
31
- ```json
32
- {
33
- "jenkins-cli": {
34
- "apiToken": "http://username:token@jenkins.example.com:8080",
35
- "job": "your-job-name",
36
- "modes": ["dev", "sit", "uat"]
37
- }
87
+ export function validateTicket(input: string) {
88
+ return /^(feat|fix|refactor)-[a-zA-Z0-9]+$/.test(input) || '工单号格式不正确';
38
89
  }
39
90
  ```
40
91
 
41
- ### 配置优先级
92
+ > **提示**: 配置也支持写在 `package.json` 的 `jenkins-cli` 字段里。
93
+ >
94
+ > **查找顺序**: `jenkins-cli.yaml` > `package.json` > 父目录的 `jenkins-cli.yaml`。
95
+
96
+ ### 🤖 命令参考
97
+
98
+ #### `trigger` (非交互式触发)
42
99
 
43
- 从高到低:
100
+ 适用于 CI 或其他脚本化场景。
44
101
 
45
- 1. 项目根目录的 `jenkins-cli.yaml`
46
- 2. 项目根目录的 `package.json` 里的 `jenkins-cli` 字段
47
- 3. 向上查找到的 `jenkins-cli.yaml`
102
+ ```bash
103
+ # 触发 dev 环境构建
104
+ jenkins-cli trigger -b origin/develop -m dev
105
+
106
+ # 同时触发多个环境
107
+ jenkins-cli trigger -b origin/develop -m dev,sit
48
108
 
49
- 高优先级配置会覆盖低优先级的同名字段。比如项目根的 YAML 里有 `job`,就不会用 `package.json` 或 上层 YAML 里的 `job`。
109
+ # 传入额外参数
110
+ jenkins-cli trigger -b origin/develop -m dev --param version=1.2.3 --param force=true
111
+ ```
50
112
 
51
- ### 使用
113
+ ---
52
114
 
53
- 在项目目录执行:
115
+ #### `builds` - 构建管理
54
116
 
55
117
  ```bash
56
- jenkins-cli
118
+ # 查看最近 20 次构建
119
+ jenkins-cli builds list
120
+
121
+ # 查看运行中的构建
122
+ jenkins-cli builds running
123
+
124
+ # 查看 Jenkins 上所有运行中的构建
125
+ jenkins-cli builds running -a
57
126
  ```
58
127
 
59
- 或在 `package.json` 添加 script:
128
+ #### `job` - Job 管理
60
129
 
61
- ```json
62
- {
63
- "scripts": {
64
- "ship": "jenkins-cli"
65
- }
66
- }
130
+ ```bash
131
+ # 列出所有 Job (支持 glob 过滤)
132
+ jenkins-cli job list --search 'project-*'
133
+
134
+ # 查看当前 Job 信息
135
+ jenkins-cli job info
136
+ ```
137
+
138
+ #### `log` - 查看日志
139
+
140
+ ```bash
141
+ # 查看指定构建号的日志
142
+ jenkins-cli log 1234
143
+
144
+ # 实时跟踪日志
145
+ jenkins-cli log 1234 -f
146
+ ```
147
+
148
+ #### `stop` - 停止构建
149
+
150
+ ```bash
151
+ # 停止一个构建
152
+ jenkins-cli stop 1234
153
+
154
+ # 清理当前 Job 的队列和所有运行中的构建
155
+ jenkins-cli stop -a
156
+
157
+ # 停止 Jenkins 上所有的构建和队列项 (慎用)
158
+ jenkins-cli stop -A
159
+ ```
160
+
161
+ #### `queue` - 等待构建队列管理
162
+
163
+ ```bash
164
+ # 查看等待构建的队列
165
+ jenkins-cli queue list
166
+
167
+ # 取消一个等待构建队列项
168
+ jenkins-cli queue cancel 5678
169
+ ```
170
+
171
+ #### `params` - Job 参数
172
+
173
+ ```bash
174
+ # 查看当前 Job 的参数定义
175
+ jenkins-cli params
67
176
  ```
68
177
 
69
- 交互示例:
178
+ #### `config` - Job 配置
179
+
180
+ ```bash
181
+ # 读取当前 Job 的 XML 配置
182
+ jenkins-cli config read
70
183
 
184
+ # 以 JSON 格式输出
185
+ jenkins-cli config read -f json
71
186
  ```
72
- 🚀 Jenkins CLI - Jenkins Deployment CLI
73
187
 
74
- Configuration loaded
75
- ✔ Found 3 branches
76
- ? 请选择要打包的分支: origin/develop
77
- ? 请选择要打包的环境: dev, sit, uat
188
+ #### `whoami` - 用户信息
78
189
 
79
- ✔ dev - Build triggered successfully
80
- sit - Build triggered successfully
81
- ✔ uat - Build triggered successfully
190
+ ```bash
191
+ # 验证 API Token 并查看当前用户
192
+ jenkins-cli whoami
82
193
  ```
194
+
195
+ ### ❓ FAQ
196
+
197
+ **Q: `stop` 或 `queue cancel` 命令返回 403 Forbidden?**
198
+
199
+ A: 通常是 Jenkins 用户权限不足 (需要 `Job > Cancel` 权限)。请确保你使用的是 API Token 而非密码,并检查 Jenkins 的 CSRF 设置。工具会自动处理 CSRF,但权限问题需要手动排查。
package/dist/cli.js CHANGED
@@ -1,24 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from 'commander';
3
- import O from 'inquirer';
4
- import a from 'chalk';
5
- import J from 'ora';
6
- import c from 'fs-extra';
7
- import F from 'js-yaml';
8
- import l from 'path';
3
+ import He from 'inquirer';
4
+ import f from 'chalk';
5
+ import Me from 'ora';
9
6
  import { exec } from 'child_process';
10
- import { promisify } from 'util';
11
- import D from 'axios';
7
+ import Ye, { promisify } from 'util';
8
+ import C from 'fs-extra';
9
+ import Re from 'js-yaml';
10
+ import S from 'path';
11
+ import Te from 'axios';
12
+ import We from 'jiti';
13
+ import { fileURLToPath } from 'url';
14
+ import { XMLParser } from 'fast-xml-parser';
12
15
 
13
- var u="jenkins-cli.yaml",d="package.json",m="jenkins-cli",v="jenkinsCli";function I(n=process.cwd()){let e=n;for(;;){let r=l.join(e,u);if(c.existsSync(r))return r;let t=l.dirname(e);if(t===e)return null;e=t;}}function T(n=process.cwd()){let e=n;for(;;){let r=l.join(e,d);if(c.existsSync(r))return e;let t=l.dirname(e);if(t===e)return null;e=t;}}function h(n){return !!(n.apiToken&&n.job&&Array.isArray(n.modes)&&n.modes.length>0)}async function w(n){let e=await c.readFile(n,"utf-8"),r=F.load(e);if(!r||typeof r!="object")throw new Error(`Invalid config in ${u}: expected a YAML object`);return r}async function L(n){let e=l.join(n,d);if(!await c.pathExists(e))return {};let r=await c.readFile(e,"utf-8"),t=JSON.parse(r),o=t[m]??t[v];if(o==null)return {};if(typeof o!="object"||Array.isArray(o))throw new Error(`Invalid ${d} config: "${m}" must be an object`);return o}async function y(){let n=process.cwd(),e=T(n)??n,r=l.join(e,u),t=await c.pathExists(r)?await w(r):{};if(h(t))return t;let o={},p=l.dirname(e),s=I(p);s&&await c.pathExists(s)&&(o=await w(s));let i=await L(e);if(o={...o,...i},o={...o,...t},!h(o))throw new Error(`Config incomplete or not found: tried ${u} in project root, "${m}" in $
14
- {PACKAGE_JSON}, and walking up from cwd. Required: apiToken, job, modes (non-empty array)`);return o}var k=promisify(exec);async function b(){try{let{stdout:n}=await k("git branch --show-current"),e=n.trim();if(!e)throw new Error("Not on any branch (detached HEAD state)");return e}catch{throw new Error("Failed to get current branch. Are you in a git repository?")}}async function C(){try{let{stdout:n}=await k('git branch -r --format="%(refname:short)"');return n.split(`
15
- `).filter(e=>e.includes("/")&&!e.includes("HEAD"))}catch{throw new Error("Failed to list branches. Are you in a git repository?")}}function E(n){let e=n.match(/^(https?):\/\/([^:]+):([^@]+)@(.+)$/);if(!e)throw new Error("Invalid apiToken format. Expected: http://username:token@host:port");let[,r,t,o,p]=e,s=`${r}://${p}`,i=D.create({baseURL:s,auth:{username:t,password:o},proxy:!1,timeout:3e4});return {baseURL:s,auth:{username:t,password:o},axios:i}}async function x(n,e,r){try{let t=await n.axios.post(`/job/${e}/buildWithParameters`,new URLSearchParams({branch:r.branch,mode:r.mode}),{headers:{"Content-Type":"application/x-www-form-urlencoded"}});if(t.status!==201)throw new Error(`Unexpected status: ${t.status}`)}catch(t){throw t.response?t.response.status===403?new Error(`Failed to trigger build: Jenkins rejected the request (HTTP 403).
16
- This might be due to:
17
- - Insufficient permissions
18
- - Jenkins Quiet Period (try again after a few seconds)
19
- - The job is already queued`):new Error(`Failed to trigger build: HTTP ${t.response.status} - ${t.response.statusText}`):t.code==="ECONNREFUSED"?new Error(`Failed to trigger build: Connection refused to ${n.baseURL}`):new Error(`Failed to trigger build: ${t.message}`)}}var j="2.0.0",A="Jenkins deployment CLI";async function R(){console.log(a.bold.blue(`
16
+ var Q="3.0.1",_="Jenkins deployment CLI";var G=promisify(exec);async function H(){try{let{stdout:n}=await G("git branch --show-current"),e=n.trim();if(!e)throw new Error("Not on any branch (detached HEAD state)");return e}catch{throw new Error("Failed to get current branch. Are you in a git repository?")}}async function V(){try{let{stdout:n}=await G('git branch -r --format="%(refname:short)"');return n.split(`
17
+ `).filter(e=>e.includes("/")&&!e.includes("HEAD"))}catch{throw new Error("Failed to list branches. Are you in a git repository?")}}var P="jenkins-cli.yaml",q="package.json",F="jenkins-cli",Ee="jenkinsCli";function z(n,e=process.cwd()){let t=e;for(;;){let r=S.join(t,n);if(C.existsSync(r))return r;let o=S.dirname(t);if(o===t)return null;t=o;}}function Pe(n=process.cwd()){return z(P,n)}function Be(n=process.cwd()){let e=z(q,n);return e?S.dirname(e):null}function Le(n){return !!(n.apiToken&&n.job&&Array.isArray(n.modes)&&n.modes.length>0)}async function Y(n){let e=await C.readFile(n,"utf-8"),t=Re.load(e);if(!t||typeof t!="object")throw new Error(`Invalid config in ${P}: expected a YAML object`);return t}async function Ie(n){let e=S.join(n,q);if(!await C.pathExists(e))return {};let t=await C.readFile(e,"utf-8"),r=JSON.parse(t),o=r[F]??r[Ee];if(o==null)return {};if(typeof o!="object"||Array.isArray(o))throw new Error(`Invalid ${q} config: "${F}" must be an object`);return o}async function X(){let n=process.cwd(),e=Be(n)??n,t=S.join(e,P),r={},o=S.dirname(e),s=Pe(o);s&&await C.pathExists(s)&&(r=await Y(s));let i=await Ie(e);r={...r,...i};let a=await C.pathExists(t)?await Y(t):{};if(r={...r,...a},!Le(r))throw new Error(`Config incomplete or not found: tried ${P} in project root, "${F}" in $
18
+ {PACKAGE_JSON}, and walking up from cwd. Required: apiToken, job, modes (non-empty array)`);return {...r,projectRoot:e}}var U={maxRedirects:0,validateStatus:n=>n>=200&&n<400};function h(n){return `job/${n.split("/").map(t=>t.trim()).filter(Boolean).map(encodeURIComponent).join("/job/")}`}function O(n){let e=Array.isArray(n)?n.find(o=>o?.parameters):void 0,t=Array.isArray(e?.parameters)?e.parameters:[],r={};for(let o of t)o?.name&&(r[String(o.name)]=o?.value);return r}function qe(n){try{let t=new URL(n).pathname.split("/").filter(Boolean),r=[];for(let o=0;o<t.length-1;o++)t[o]==="job"&&t[o+1]&&r.push(decodeURIComponent(t[o+1]));return r.join("/")}catch{return ""}}async function Fe(n,e){let r=(await n.axios.get(new URL("api/json?tree=number,url,building,result,timestamp,duration,estimatedDuration,fullDisplayName,displayName,actions[parameters[name,value]]",e).toString())).data??{};return {...r,parameters:O(r.actions)}}function Ue(n,e=400){try{let r=(typeof n=="string"?n:JSON.stringify(n??"",null,2)).trim();return r?r.length>e?`${r.slice(0,e)}...`:r:""}catch{return ""}}function ee(n){let e=n.match(/^(https?):\/\/([^:]+):([^@]+)@(.+)$/);if(!e)throw new Error("Invalid apiToken format. Expected: http(s)://username:token@host:port");let[,t,r,o,s]=e,i=`${t}://${s.replace(/\/$/,"")}/`,a=Te.create({baseURL:i,auth:{username:r,password:o},proxy:!1,timeout:3e4});return {baseURL:i,auth:{username:r,password:o},axios:a,crumbForm:void 0}}async function te(n){try{let e=await n.axios.get("crumbIssuer/api/json"),{data:t}=e;t?.crumbRequestField&&t?.crumb&&(n.axios.defaults.headers.common[t.crumbRequestField]=t.crumb,n.crumbForm={field:t.crumbRequestField,value:t.crumb});let r=e.headers["set-cookie"];if(r){let o=Array.isArray(r)?r.map(s=>s.split(";")[0].trim()):[String(r).split(";")[0].trim()];n.axios.defaults.headers.common.Cookie=o.join("; ");}}catch{}}async function J(n,e){let r=(await n.axios.get("queue/api/json?tree=items[id,task[name],actions[parameters[name,value]],why]")).data.items;if(e){let o=e.trim(),s=o.split("/").filter(Boolean).pop();return r.filter(i=>{let a=i.task?.name;return a===o||(s?a===s:!1)})}return r}async function N(n,e){let t=await n.axios.get(`${h(e)}/api/json?tree=builds[number,url,building,result,timestamp,duration,estimatedDuration,actions[parameters[name,value]]]`);return (Array.isArray(t.data?.builds)?t.data.builds:[]).filter(o=>o?.building).map(o=>({...o,parameters:O(o.actions)}))}async function B(n){let e=await n.axios.get("computer/api/json?tree=computer[displayName,executors[currentExecutable[number,url]],oneOffExecutors[currentExecutable[number,url]]]"),t=Array.isArray(e.data?.computer)?e.data.computer:[],r=[];for(let i of t){let a=Array.isArray(i?.executors)?i.executors:[],l=Array.isArray(i?.oneOffExecutors)?i.oneOffExecutors:[];for(let u of [...a,...l]){let c=u?.currentExecutable;c?.url&&r.push({number:Number(c.number),url:String(c.url)});}}let o=new Map;for(let i of r)i.url&&o.set(i.url,i);return (await Promise.all([...o.values()].map(async i=>{let a=await Fe(n,i.url),l=qe(String(a?.url??i.url));return {...a,job:l}}))).filter(i=>i?.building)}async function A(n,e){return await n.axios.post("queue/cancelItem",new URLSearchParams({id:e.toString()}),U),!0}async function R(n,e,t){try{await n.axios.post(`${h(e)}/${t}/stop/`,new URLSearchParams({}),U);}catch(r){let o=r.response?.status;if(o===403){let s=typeof r.response?.data=="string"?r.response.data.slice(0,200):JSON.stringify(r.response?.data??"").slice(0,200),i=/crumb|csrf/i.test(s);console.log(f.red("[Error] 403 Forbidden: Jenkins \u62D2\u7EDD\u505C\u6B62\u6784\u5EFA\u8BF7\u6C42\uFF08\u53EF\u80FD\u662F\u6743\u9650\u6216 CSRF\uFF09\u3002")),console.log(f.yellow('[Hint] \u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237\u5BF9\u8BE5 Job \u62E5\u6709 "Job" -> "Cancel" \u6743\u9650\u3002')),console.log(i?"[Hint] Jenkins returned a crumb/CSRF error. Ensure crumb + session cookie are sent, or use API token (not password).":s?`[Hint] Jenkins response: ${s}`:'[Hint] If "Job/Cancel" is already granted, also try granting "Run" -> "Update" (some versions use it for stop).');return}if(o===404||o===400||o===500){console.log(`[Info] Build #${t} could not be stopped (HTTP ${o}). Assuming it finished.`);return}throw r}}async function L(n,e,t){let r="branch",o="mode",s=t[o];s&&console.log(`Checking for conflicting builds for mode: ${s}...`);let[i,a]=await Promise.all([J(n,e),N(n,e)]),l=(m,p)=>{let y=m.actions?.find(v=>v.parameters);return y?y.parameters.find(v=>v.name===p)?.value:void 0},u=t[r];if(s&&u){let m=i.find(p=>l(p,o)===s&&l(p,r)===u);if(m)return console.log(`Build already in queue (ID: ${m.id}). Skipping trigger.`),new URL(`queue/item/${m.id}/`,n.baseURL).toString()}let c=!1;if(s)for(let m of a)l(m,o)===s&&(console.log(`Stopping running build #${m.number} (mode=${s})...`),await R(n,e,m.number),c=!0);c&&await new Promise(m=>setTimeout(m,1e3)),s&&console.log(`Triggering new build for mode=${s}...`);try{return (await n.axios.post(`${h(e)}/buildWithParameters/`,new URLSearchParams({...t}),U)).headers.location||""}catch(m){let p=m?.response?.status,y=Ue(m?.response?.data);if(p===400){let w=[];w.push("Hint: Jenkins returned 400. Common causes: job is not parameterized, parameter names do not match, or the job endpoint differs (e.g. multibranch jobs)."),w.push(`Hint: This CLI sends parameters "${r}" and "${o}". Ensure your Jenkins Job has matching parameter names.`);let v=y?`
19
+ Jenkins response (snippet):
20
+ ${y}`:"";throw new Error(`Request failed with status code 400.${v}
21
+ ${w.join(`
22
+ `)}`)}if(p){let w=y?`
23
+ Jenkins response (snippet):
24
+ ${y}`:"";throw new Error(`Request failed with status code ${p}.${w}`)}throw m}}async function ne(n){return (await n.axios.get("whoAmI/api/json")).data}async function re(n){return (await n.axios.get("api/json?tree=jobs[name,url,color]")).data.jobs??[]}async function oe(n,e){return (await n.axios.get(`${h(e)}/api/json?tree=name,fullName,url,buildable,inQueue,nextBuildNumber,color,healthReport[description,score],lastBuild[number,url,building,result,timestamp,duration],lastCompletedBuild[number,url,result,timestamp,duration]`)).data}async function ie(n,e,t){let r=Math.max(1,t?.limit??20);return ((await n.axios.get(`${h(e)}/api/json?tree=builds[number,url,building,result,timestamp,duration,actions[parameters[name,value]]]{0,${r}}`)).data.builds??[]).map(i=>({number:i.number,result:i.result,building:i.building,timestamp:i.timestamp,duration:i.duration,url:i.url,parameters:O(i.actions)}))}async function se(n,e,t){let r=await n.axios.get(`${h(e)}/${t}/consoleText`,{responseType:"text"});return String(r.data??"")}async function ae(n,e,t,r){let o=Math.max(0,r?.start??0),s=Math.max(200,r?.intervalMs??1e3);for(;;){let i=await n.axios.get(`${h(e)}/${t}/logText/progressiveText?start=${o}`,{responseType:"text"}),a=String(i.data??"");a&&process.stdout.write(a);let l=i.headers["x-text-size"],u=i.headers["x-more-data"],c=l?Number(l):NaN;if(Number.isNaN(c)||(o=c),!(String(u??"").toLowerCase()==="true"))break;await new Promise(p=>setTimeout(p,s));}}async function ce(n,e){return ((await n.axios.get(`${h(e)}/api/json?tree=property[parameterDefinitions[name,type,description,defaultParameterValue[value],choices]]`)).data?.property??[]).flatMap(s=>s?.parameterDefinitions??[]).filter(Boolean).map(s=>({name:s.name,type:s.type,description:s.description,default:s.defaultParameterValue?.value,choices:s.choices}))}async function le(n,e){let t=await n.axios.get(`${h(e)}/config.xml`,{responseType:"text"});return String(t.data??"")}async function g(){let n=Me("Loading configuration...").start();try{let e=await X();n.succeed("Configuration loaded");let t=ee(e.apiToken);return await te(t),{config:e,jenkins:t}}catch(e){n.fail(f.red(e.message)),process.exit(1);}}var Qe=We(fileURLToPath(import.meta.url),{interopDefault:!0});async function M(n){return await Promise.resolve(n)}function _e(n){let e=String(n??"").trim();if(!e)return null;let t=e.split(":");if(t.length<2)return null;if(t.length===2){let[i,a]=t;return !i||!a?null:{file:i,kind:"Var",exportName:a}}let r=t.at(-2),o=t.at(-1);if(r!=="Fun"&&r!=="Var"||!o)return null;let s=t.slice(0,-2).join(":");return s?{file:s,kind:r,exportName:o}:null}function Ge(n,e){return S.isAbsolute(e)?e:S.join(n,e)}function de(n){return !!n&&typeof n=="object"&&!Array.isArray(n)}async function pe(n,e){let t=_e(e);if(!t)return null;let r=Ge(n,t.file);if(!await C.pathExists(r))throw new Error(`configs reference not found: ${t.file}`);let o=Qe(r),s=de(o)?o:{default:o},i=t.exportName==="default"?s.default:s[t.exportName];if(i===void 0)throw new Error(`configs reference export not found: ${e}`);return {kind:t.kind,value:i}}async function me(n,e){if(typeof e!="string")return e;let t=await pe(n,e);if(!t)return e;if(t.kind==="Var"){if(typeof t.value=="function"){let o=t.value();return await M(o)}return await M(t.value)}if(typeof t.value!="function")throw new Error(`configs reference expected a function: ${e}`);let r=t.value();return await M(r)}async function fe(n,e){let t=Array.isArray(n)?n:[];if(t.length===0)return [];let r=String(e.projectRoot??"").trim()||process.cwd(),o=[];for(let s of t){if(!de(s))continue;let i={...s};if(i.choices!==void 0){i.choices=await me(r,i.choices);let a=String(i.type??"").toLowerCase(),l=i.choices;if((a==="list"||a==="rawlist"||a==="checkbox")&&typeof l!="function"&&!Array.isArray(l))if(l==null)i.choices=[];else if(typeof l=="string"||typeof l=="number"||typeof l=="boolean")i.choices=[String(l)];else if(typeof l[Symbol.iterator]=="function")i.choices=Array.from(l).map(c=>String(c));else throw new Error(`configs "${String(i.name??"")}" choices must resolve to an array (got ${typeof l})`)}if(i.default!==void 0&&(i.default=await me(r,i.default)),i.validate!==void 0){let a=i.validate;if(typeof a=="string"){let l=await pe(r,a);if(!l)i.validate=a;else if(l.kind==="Var"){if(typeof l.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=l.value;}else {if(typeof l.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=(u,c)=>l.value(u,c);}}else typeof a=="function"&&(i.validate=a);}o.push(i);}return o}function ge(n,e){let t=new Set(e),r={};for(let[o,s]of Object.entries(n))if(!t.has(o)&&s!==void 0){if(s===null){r[o]="";continue}if(Array.isArray(s)){r[o]=s.map(i=>String(i)).join(",");continue}if(typeof s=="object"){try{r[o]=JSON.stringify(s);}catch{r[o]=String(s);}continue}r[o]=String(s);}return r}async function be(){console.log(f.bold.blue(`
20
25
  \u{1F680} Jenkins CLI - Jenkins Deployment CLI
21
- `));let n=J("Loading configuration...").start(),e=await y().catch(i=>{n.fail(a.red(i.message)),process.exit(1);});n.succeed("Configuration loaded"),n.start("Fetching git branches...");let[r,t]=await Promise.all([C(),b()]).catch(i=>{n.fail(a.red(i.message)),process.exit(1);});n.succeed(`Found ${r.length} branches`);let o;try{o=await O.prompt([{type:"list",name:"branch",message:"\u8BF7\u9009\u62E9\u8981\u6253\u5305\u7684\u5206\u652F:",choices:r,default:r.indexOf(t)},{type:"checkbox",name:"modes",message:"\u8BF7\u9009\u62E9\u8981\u6253\u5305\u7684\u73AF\u5883:",choices:e.modes,validate:i=>i.length===0?"You must select at least one environment":!0}]);}catch{console.log(a.yellow(`
22
- \u{1F44B} Cancelled by user`)),process.exit(0);}console.log();let p=E(e.apiToken),s=o.modes.map(async i=>{let f=J(`Triggering build for ${a.yellow(i)}...`).start();try{await x(p,e.job,{branch:o.branch,mode:i}),f.succeed(`${a.green(i)} - Build triggered successfully`);}catch(g){throw f.fail(`${a.red(i)} - ${g.message}`),g}});try{await Promise.all(s),console.log();}catch{console.log(a.bold.red(`
26
+ `));let{config:n,jenkins:e}=await g(),[t,r]=await Promise.all([V(),H()]),o=()=>{console.log(f.yellow(`
27
+ \u{1F44B} Cancelled by user`)),process.exit(130);},s,i={};try{process.prependOnceListener("SIGINT",o);let l=await fe(n.configs,{projectRoot:n.projectRoot}),u=new Set(["branch","modes","mode"]);for(let m of l){let p=String(m?.name??"").trim();if(u.has(p))throw new Error(`configs name "${p}" is reserved. Use another name.`)}let c=await He.prompt([{type:"list",name:"branch",message:"\u8BF7\u9009\u62E9\u8981\u6253\u5305\u7684\u5206\u652F:",choices:t,default:t.indexOf(r)},{type:"checkbox",name:"modes",message:"\u8BF7\u9009\u62E9\u8981\u6253\u5305\u7684\u73AF\u5883:",choices:n.modes,validate:m=>m.length===0?"You must select at least one environment":!0},...l]);s={branch:String(c.branch??""),modes:c.modes??[]},i=ge(c,["branch","modes"]);}catch(l){let u=l,c=u?.message?String(u.message):String(u);((u?.name?String(u.name):"")==="ExitPromptError"||c.toLowerCase().includes("cancelled")||c.toLowerCase().includes("canceled"))&&(console.log(f.yellow(`
28
+ \u{1F44B} Cancelled by user`)),process.exit(0)),console.log(f.red(`
29
+ \u274C Prompt failed: ${c}
30
+ `)),process.exit(1);}finally{process.off("SIGINT",o);}console.log();let a=s.modes.map(async l=>{let u=Me(`Triggering build for ${f.yellow(l)}...`).start();try{let c=await L(e,n.job,{...i,branch:s.branch,mode:l});u.succeed(`${f.green(l)} - Triggered! Queue: ${c}`);}catch(c){throw u.fail(`${f.red(l)} - ${c.message}`),c}});try{await Promise.all(a),console.log();}catch{console.log(f.bold.red(`
23
31
  \u274C Some builds failed. Check the output above.
24
- `)),process.exit(1);}}program.name("jenkins-cli").description(A).version(j,"-v, --version").helpOption("-h, --help").action(R);program.parse();
32
+ `)),process.exit(1);}}function b(n,e){return (e??"").trim()||n}function k(n,e){let t=Number(n);if(!Number.isInteger(t)||t<0)throw new Error(`${e} must be a non-negative integer`);return t}function he(n){let e=n.command("queue").description("\u7B49\u5F85\u6784\u5EFA\u961F\u5217\u76F8\u5173\u64CD\u4F5C");e.command("list").description("\u83B7\u53D6\u7B49\u5F85\u6784\u5EFA\u7684\u961F\u5217\u5217\u8868").option("-j, --job <job>","\u4EC5\u663E\u793A\u6307\u5B9A Job \u7684\u7B49\u5F85\u6784\u5EFA\u961F\u5217\u9879").action(async t=>{let{config:r,jenkins:o}=await g(),s=t.job?b(r.job,t.job):void 0,i=await J(o,s);console.table((i??[]).map(a=>({id:a.id,job:a.task?.name,why:a.why})));}),e.command("cancel").description("\u53D6\u6D88\u7B49\u5F85\u6784\u5EFA\u7684\u961F\u5217\u9879").argument("<id>").action(async t=>{let{jenkins:r}=await g(),o=await A(r,k(t,"id"));console.log(o?f.green("Cancelled"):f.red("Cancel failed"));});}function d(n){return {[Ye.inspect.custom]:()=>n}}function j(n){let e=String(n??""),t=e.trim();if(!t)return d(f.gray("-"));let r=s=>d(s),o=t.toLowerCase();if(o.startsWith("http://")||o.startsWith("https://"))try{let i=(new URL(t).hostname??"").toLowerCase(),a=i==="localhost"||i==="127.0.0.1"||i==="::1",l=/^10\./.test(i)||/^192\.168\./.test(i)||/^127\./.test(i)||/^172\.(1[6-9]|2\d|3[0-1])\./.test(i);return r(a||l?f.cyanBright(e):f.hex("#4EA1FF")(e))}catch{return r(f.hex("#4EA1FF")(e))}return o.startsWith("ws://")||o.startsWith("wss://")?r(f.cyan(e)):o.startsWith("ssh://")||o.startsWith("git+ssh://")||o.startsWith("git@")?r(f.magenta(e)):o.startsWith("file://")?r(f.yellow(e)):o.startsWith("mailto:")?r(f.green(e)):o.startsWith("javascript:")||o.startsWith("data:")?r(f.red(e)):t.startsWith("/")||t.startsWith("./")||t.startsWith("../")?r(f.cyan(e)):/^[a-z0-9.-]+(?::\d+)?(\/|$)/i.test(t)?r(f.blue(e)):r(e)}function $(n){let e=new Date(n),t=r=>String(r).padStart(2,"0");return `${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())} ${t(e.getHours())}:${t(e.getMinutes())}:${t(e.getSeconds())}`}function E(n,e){if(e===!0)return d(f.cyan("BUILDING"));let t=String(n??"").toUpperCase();return d(t?t==="SUCCESS"?f.green(t):t==="ABORTED"?f.gray(t):t==="FAILURE"?f.red(t):t==="UNSTABLE"?f.yellow(t):t==="NOT_BUILT"?f.gray(t):t:"-")}function D(n){let e=String(n??"");if(!e)return d("-");let t=e.endsWith("_anime"),r=t?e.slice(0,-6):e,o=(()=>{switch(r){case"blue":return t?f.blueBright(e):f.blue(e);case"red":return t?f.redBright(e):f.red(e);case"yellow":return t?f.yellowBright(e):f.yellow(e);case"aborted":return f.gray(e);case"disabled":case"notbuilt":case"grey":case"gray":return f.gray(e);default:return e}})();return d(o)}function ze(n){let e=Object.entries(n??{}).filter(([r])=>r);if(!e.length)return d("-");e.sort(([r],[o])=>r.localeCompare(o,"en",{sensitivity:"base"}));let t=e.map(([r,o])=>{if(o===void 0)return `${r}=`;if(o===null)return `${r}=null`;if(typeof o=="string"||typeof o=="number"||typeof o=="boolean")return `${r}=${o}`;try{return `${r}=${JSON.stringify(o)}`}catch{return `${r}=${String(o)}`}}).join(", ");return d(t)}function we(n){let e=n.command("builds").description("\u6784\u5EFA\u76F8\u5173\u64CD\u4F5C");e.command("list").description("\u83B7\u53D6\u6784\u5EFA\u5217\u8868").option("-j, --job <job>","\u6307\u5B9A job").option("-n, --limit <n>","\u6570\u91CF","20").action(async t=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job),i=await ie(o,s,{limit:Number(t.limit)});console.table(i.map(a=>({number:a.number,result:E(a.result,a.building),building:a.building,durationS:Number((Number(a.duration??0)/1e3).toFixed(3)),date:d($(Number(a.timestamp??0)))})));}),e.command("running").description("\u83B7\u53D6\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").option("-j, --job <job>","\u6307\u5B9A job").option("-a, --all","\u67E5\u8BE2 Jenkins \u4E0B\u6240\u6709\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").action(async t=>{let{config:r,jenkins:o}=await g(),s=t.all?await B(o):await N(o,b(r.job,t.job));console.table((s??[]).map(i=>({...t.all?{job:d(String(i.job??""))}:{},number:i.number,date:d(i.timestamp?$(Number(i.timestamp)):"-"),parameters:ze(i.parameters),url:j(String(i.url??""))})));});}function xe(n){n.command("log").description("\u83B7\u53D6\u6784\u5EFA\u63A7\u5236\u53F0\u65E5\u5FD7").argument("<id>").option("-j, --job <job>","\u6307\u5B9A job").option("--tail <n>","\u4EC5\u8F93\u51FA\u6700\u540E N \u884C").option("-f, --follow","\u6301\u7EED\u8F93\u51FA\uFF08\u76F4\u5230 Jenkins \u8868\u793A\u65E0\u66F4\u591A\u65E5\u5FD7\uFF09").option("--interval <ms>","follow \u62C9\u53D6\u95F4\u9694","1000").action(async(e,t)=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job),i=k(e,"id");if(t.follow){await ae(o,s,i,{intervalMs:Number(t.interval)});return}let a=await se(o,s,i);if(t.tail){let l=k(t.tail,"tail"),u=a.split(`
33
+ `);console.log(u.slice(Math.max(0,u.length-l)).join(`
34
+ `));return}process.stdout.write(a);});}function ke(n){n.command("stop").description("\u6E05\u7406\u7B49\u5F85\u6784\u5EFA\u7684\u961F\u5217\u4E2D\u7684\u4EFB\u52A1\u5E76\u505C\u6B62\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").argument("[id]","\u6784\u5EFA\u7F16\u53F7").option("-j, --job <job>","\u6307\u5B9A Jenkins Job").option("-a, --all","\u53D6\u6D88\u8BE5 Job \u7684\u7B49\u5F85\u6784\u5EFA\u961F\u5217\u9879\uFF0C\u5E76\u505C\u6B62\u8BE5 Jenkins Job \u4E0B\u6240\u6709\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").option("-A, --ALL","\u53D6\u6D88 Jenkins \u4E0A\u6240\u6709\u7B49\u5F85\u6784\u5EFA\u961F\u5217\u9879\uFF0C\u5E76\u505C\u6B62\u6240\u6709\u6B63\u5728\u8FD0\u884C\u7684\u6784\u5EFA").action(async(e,t)=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job);if(t.ALL){let[i,a]=await Promise.all([J(o),B(o)]),l=(i??[]).map(c=>Number(c.id)).filter(c=>!Number.isNaN(c)),u=(a??[]).map(c=>({number:Number(c.number),job:String(c.job??"")})).filter(c=>c.job&&!Number.isNaN(c.number));for(let c of l)await A(o,c);for(let c of u)await R(o,c.job,c.number);console.log(f.green(`Requested: cancelled ${l.length} queue item(s), stopped ${u.length} running build(s).`));return}if(t.all){let[i,a]=await Promise.all([J(o,s),N(o,s)]),l=(i??[]).map(c=>Number(c.id)).filter(c=>!Number.isNaN(c)),u=(a??[]).map(c=>Number(c.number)).filter(c=>!Number.isNaN(c));for(let c of l)await A(o,c);for(let c of u)await R(o,s,c);console.log(f.green(`Requested: cancelled ${l.length} queue item(s), stopped ${u.length} running build(s).`));return}if(!e){console.log(f.red("Missing build id. Provide <id> or use -a/--all."));return}await R(o,s,k(e,"id")),console.log(f.green("Stop requested"));});}function je(n){n.command("params").description("\u83B7\u53D6 job \u53C2\u6570\u5B9A\u4E49").option("-j, --job <job>","\u6307\u5B9A job").action(async e=>{let{config:t,jenkins:r}=await g(),o=b(t.job,e.job),s=await ce(r,o),i=Math.max(0,...s.map(a=>String(a?.name??"").length));console.table((s??[]).map(a=>({name:d(String(a?.name??"").padEnd(i," ")),type:d(String(a?.type??"")),description:d(String(a?.description??"")),default:d(a?.default===void 0?"-":String(a.default)),choices:d(Array.isArray(a?.choices)?a.choices.join(", "):a?.choices??"-")})));});}function Ce(n){n.command("whoami").description("\u67E5\u770B\u5F53\u524D Token \u5BF9\u5E94\u7684 Jenkins \u7528\u6237\u4FE1\u606F").action(async()=>{let{jenkins:e}=await g(),t=await ne(e),r=t&&typeof t=="object"?t:{value:t},o=(u,c)=>{let m=u.length;if(m>=c)return u;let p=c-m,y=Math.floor(p/2),w=p-y;return `${" ".repeat(y)}${u}${" ".repeat(w)}`},s=r.name??r.fullName??r.id??"",i=s==null?"":String(s),a=Math.max(20,i.length),l={};l.name=d(o(i,a));for(let u of Object.keys(r).sort((c,m)=>c.localeCompare(m,"en"))){if(u==="name")continue;let c=r[u],m=u.toLowerCase();if((m==="url"||m.endsWith("url")||m.includes("url"))&&(typeof c=="string"||typeof c=="number")){l[u]=j(String(c));continue}let y=c==null?"":typeof c=="object"?JSON.stringify(c):String(c);l[u]=d(y);}console.table([l]);});}function Se(n){n.command("config").description("\u8BFB\u53D6 Jenkins Job \u914D\u7F6E").command("read").description("\u8BFB\u53D6 job \u914D\u7F6E").option("-j, --job <job>","\u6307\u5B9A job").option("-f, --format <format>","\u8F93\u51FA\u683C\u5F0F\uFF1Axml \u6216 json","xml").action(async t=>{let{config:r,jenkins:o}=await g(),s=b(r.job,t.job),i=String(t.format??"xml").toLowerCase();if(i!=="xml"&&i!=="json")throw new Error(`Invalid format: ${t.format}. Expected: xml or json`);let a=await le(o,s);if(i==="xml"){process.stdout.write(a);return}let u=new XMLParser({ignoreAttributes:!1,attributeNamePrefix:"@_"}).parse(a);console.log(JSON.stringify(u,null,2));});}function Je(n){let e=n.command("job").description("Job \u76F8\u5173\u64CD\u4F5C"),t=(r,o)=>{let s=String(o??"").trim();if(!s)return !0;if(!/[*?]/.test(s))return r.includes(s);let l=`^${s.replace(/[$()*+.?[\\\]^{|}]/g,"\\$&").replace(/\\\*/g,".*").replace(/\\\?/g,".")}$`;try{return new RegExp(l).test(r)}catch{return r.includes(s)}};e.command("list").description("\u83B7\u53D6\u6240\u6709 job").option("--search <keyword>","\u6309\u540D\u79F0\u8FC7\u6EE4\uFF08\u652F\u6301 glob\uFF09").action(async r=>{let{jenkins:o}=await g(),s=await re(o),i=(r.search??"").trim(),a=i?s.filter(c=>t(String(c.name??""),i)):s;a.sort((c,m)=>String(c?.name??"").localeCompare(String(m?.name??""),"en",{sensitivity:"base"}));let l=Math.max(0,...a.map(c=>String(c?.name??"").length)),u=Math.max(0,...a.map(c=>String(c?.url??"").length));console.table((a??[]).map(c=>({name:d(String(c.name??"").padEnd(l," ")),color:D(c.color),url:j(String(c.url??"").padEnd(u," "))})));}),e.command("info").description("\u83B7\u53D6 job \u4FE1\u606F").option("-j, --job <job>","\u6307\u5B9A job").option("--json","\u8F93\u51FA\u539F\u59CB JSON").action(async r=>{let{config:o,jenkins:s}=await g(),i=b(o.job,r.job),a=await oe(s,i);if(r.json){console.log(JSON.stringify(a,null,2));return}let l=a?.lastBuild,u=a?.lastCompletedBuild,c=Array.isArray(a?.healthReport)?a.healthReport:[];console.table([{name:d(String(a?.name??"")),url:j(String(a?.url??"")),color:D(a?.color),buildable:a?.buildable,inQueue:a?.inQueue,nextBuildNumber:a?.nextBuildNumber,healthScore:c[0]?.score??"-",lastBuild:d(l?.number?`#${l.number}`:"-"),lastResult:E(l?.result,l?.building),lastDurationS:l?.duration===void 0?"-":Number((Number(l.duration)/1e3).toFixed(3)),lastDate:d(l?.timestamp?$(Number(l.timestamp)):"-"),lastCompleted:d(u?.number?`#${u.number}`:"-"),lastCompletedResult:E(u?.result,!1),lastCompletedDate:d(u?.timestamp?$(Number(u.timestamp)):"-")}]);});}function et(n){return n.split(",").map(e=>e.trim()).filter(Boolean)}function tt(n){let e=n.indexOf("=");if(e<=0)throw new Error(`Invalid --param: "${n}". Expected: key=value`);let t=n.slice(0,e).trim(),r=n.slice(e+1).trim();if(!t)throw new Error(`Invalid --param: "${n}". Key is empty`);return {key:t,value:r}}function $e(n){n.command("trigger").description("\u975E\u4EA4\u4E92\u89E6\u53D1 Jenkins \u6784\u5EFA").requiredOption("-b, --branch <branch>","\u5206\u652F\uFF08\u4F8B\u5982 origin/develop\uFF09").option("-m, --mode <mode>","\u90E8\u7F72\u73AF\u5883\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u6216\u7528\u9017\u53F7\u5206\u9694\uFF1A-m dev -m uat / -m dev,uat\uFF09",(e,t)=>t.concat(et(e)),[]).option("--param <key=value>","\u989D\u5916\u53C2\u6570\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u4F8B\u5982\uFF1A--param foo=bar\uFF09",(e,t)=>t.concat(e),[]).option("-j, --job <job>","\u6307\u5B9A job").action(async e=>{let{config:t,jenkins:r}=await g(),o=b(t.job,e.job),s=String(e.branch??"").trim();if(!s)throw new Error("branch is required");let i=(e.mode??[]).map(String).map(c=>c.trim()).filter(Boolean),a=Array.from(new Set(i));if(a.length===0)throw new Error("mode is required. Example: jenkins-cli trigger -b origin/develop -m dev -m uat");let l={};for(let c of e.param??[]){let{key:m,value:p}=tt(String(c));l[m]=p;}console.log();let u=a.map(async c=>{let m=Me(`Triggering build for ${f.yellow(c)}...`).start();try{let p=await L(r,o,{...l,branch:s,mode:c});m.succeed(`${f.green(c)} - Triggered! Queue: ${p}`);}catch(p){throw m.fail(`${f.red(c)} - ${p.message}`),p}});try{await Promise.all(u),console.log();}catch{console.log(f.bold.red(`
35
+ \u274C Some builds failed. Check the output above.
36
+ `)),process.exit(1);}});}function ve(n){we(n),Se(n),Je(n),xe(n),je(n),he(n),ke(n),$e(n),Ce(n);}program.name("jenkins-cli").description(_).version(Q,"-v, --version").helpOption("-h, --help").action(be);ve(program);program.parse();
package/dist/index.d.ts CHANGED
@@ -4,6 +4,15 @@ interface JenkinsConfig {
4
4
  apiToken: string;
5
5
  job: string;
6
6
  modes: string[];
7
+ /**
8
+ * Extra inquirer questions for interactive default command.
9
+ * Loaded from project root `jenkins-cli.yaml` (or package.json config).
10
+ */
11
+ configs?: ExtraPromptConfig[];
12
+ /**
13
+ * Internal: resolved project root path used to locate `jenkins-cli.yaml`.
14
+ */
15
+ projectRoot?: string;
7
16
  }
8
17
  interface BuildParams {
9
18
  branch: string;
@@ -13,6 +22,25 @@ interface UserChoices {
13
22
  branch: string;
14
23
  modes: string[];
15
24
  }
25
+ type InquirerType = 'input' | 'number' | 'confirm' | 'list' | 'rawlist' | 'checkbox' | 'password';
26
+ /**
27
+ * YAML-friendly prompt config (subset of inquirer question options).
28
+ *
29
+ * - `choices` can be an array, or a reference string like:
30
+ * - `src/utils/abc.ts:Fun:hello` (use `hello()` return value)
31
+ * - `src/utils/abc.ts:Var:hello` (use exported `hello` value)
32
+ * - `validate` can be a reference string in the same format.
33
+ */
34
+ interface ExtraPromptConfig {
35
+ type: InquirerType;
36
+ name: string;
37
+ message: string;
38
+ choices?: unknown;
39
+ default?: unknown;
40
+ validate?: unknown;
41
+ /** Pass-through: any other inquirer options */
42
+ [key: string]: unknown;
43
+ }
16
44
 
17
45
  declare function loadConfig(): Promise<JenkinsConfig>;
18
46
 
@@ -26,8 +54,18 @@ interface JenkinsClient {
26
54
  password: string;
27
55
  };
28
56
  axios: AxiosInstance;
57
+ /** crumb(部分 Jenkins 端点除 header 外也要求携带) */
58
+ crumbForm?: {
59
+ field: string;
60
+ value: string;
61
+ };
29
62
  }
30
63
  declare function createJenkinsClient(apiToken: string): JenkinsClient;
31
- declare function triggerBuild(jenkins: JenkinsClient, job: string, params: BuildParams): Promise<void>;
64
+ declare function triggerBuild(jenkins: JenkinsClient, job: string, params: BuildParams): Promise<string>;
65
+
66
+ declare function resolveExtraQuestions(configs: ExtraPromptConfig[] | undefined, options: {
67
+ projectRoot?: string;
68
+ }): Promise<Record<string, unknown>[]>;
69
+ declare function answersToJenkinsParams(answers: Record<string, unknown>, excludeKeys: string[]): Record<string, string>;
32
70
 
33
- export { BuildParams, JenkinsConfig, UserChoices, createJenkinsClient, getAllBranches, getCurrentBranch, loadConfig, triggerBuild };
71
+ export { BuildParams, JenkinsConfig, UserChoices, answersToJenkinsParams, createJenkinsClient, getAllBranches, getCurrentBranch, loadConfig, resolveExtraQuestions, triggerBuild };
package/dist/index.js CHANGED
@@ -1,16 +1,21 @@
1
- import i from 'fs-extra';
2
- import h from 'js-yaml';
3
- import s from 'path';
1
+ import d from 'fs-extra';
2
+ import B from 'js-yaml';
3
+ import g from 'path';
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
- import J from 'axios';
6
+ import K from 'axios';
7
+ import R from 'chalk';
8
+ import Z from 'jiti';
9
+ import { fileURLToPath } from 'url';
7
10
 
8
- var c="jenkins-cli.yaml",f="package.json",p="jenkins-cli",w="jenkinsCli";function y(n=process.cwd()){let t=n;for(;;){let e=s.join(t,c);if(i.existsSync(e))return e;let r=s.dirname(t);if(r===t)return null;t=r;}}function C(n=process.cwd()){let t=n;for(;;){let e=s.join(t,f);if(i.existsSync(e))return t;let r=s.dirname(t);if(r===t)return null;t=r;}}function d(n){return !!(n.apiToken&&n.job&&Array.isArray(n.modes)&&n.modes.length>0)}async function g(n){let t=await i.readFile(n,"utf-8"),e=h.load(t);if(!e||typeof e!="object")throw new Error(`Invalid config in ${c}: expected a YAML object`);return e}async function k(n){let t=s.join(n,f);if(!await i.pathExists(t))return {};let e=await i.readFile(t,"utf-8"),r=JSON.parse(e),o=r[p]??r[w];if(o==null)return {};if(typeof o!="object"||Array.isArray(o))throw new Error(`Invalid ${f} config: "${p}" must be an object`);return o}async function P(){let n=process.cwd(),t=C(n)??n,e=s.join(t,c),r=await i.pathExists(e)?await g(e):{};if(d(r))return r;let o={},u=s.dirname(t),a=y(u);a&&await i.pathExists(a)&&(o=await g(a));let l=await k(t);if(o={...o,...l},o={...o,...r},!d(o))throw new Error(`Config incomplete or not found: tried ${c} in project root, "${p}" in $
9
- {PACKAGE_JSON}, and walking up from cwd. Required: apiToken, job, modes (non-empty array)`);return o}var m=promisify(exec);async function b(){try{let{stdout:n}=await m("git branch --show-current"),t=n.trim();if(!t)throw new Error("Not on any branch (detached HEAD state)");return t}catch{throw new Error("Failed to get current branch. Are you in a git repository?")}}async function A(){try{let{stdout:n}=await m('git branch -r --format="%(refname:short)"');return n.split(`
10
- `).filter(t=>t.includes("/")&&!t.includes("HEAD"))}catch{throw new Error("Failed to list branches. Are you in a git repository?")}}function j(n){let t=n.match(/^(https?):\/\/([^:]+):([^@]+)@(.+)$/);if(!t)throw new Error("Invalid apiToken format. Expected: http://username:token@host:port");let[,e,r,o,u]=t,a=`${e}://${u}`,l=J.create({baseURL:a,auth:{username:r,password:o},proxy:!1,timeout:3e4});return {baseURL:a,auth:{username:r,password:o},axios:l}}async function F(n,t,e){try{let r=await n.axios.post(`/job/${t}/buildWithParameters`,new URLSearchParams({branch:e.branch,mode:e.mode}),{headers:{"Content-Type":"application/x-www-form-urlencoded"}});if(r.status!==201)throw new Error(`Unexpected status: ${r.status}`)}catch(r){throw r.response?r.response.status===403?new Error(`Failed to trigger build: Jenkins rejected the request (HTTP 403).
11
- This might be due to:
12
- - Insufficient permissions
13
- - Jenkins Quiet Period (try again after a few seconds)
14
- - The job is already queued`):new Error(`Failed to trigger build: HTTP ${r.response.status} - ${r.response.statusText}`):r.code==="ECONNREFUSED"?new Error(`Failed to trigger build: Connection refused to ${n.baseURL}`):new Error(`Failed to trigger build: ${r.message}`)}}
11
+ var h="jenkins-cli.yaml",b="package.json",x="jenkins-cli",N="jenkinsCli";function A(e,n=process.cwd()){let t=n;for(;;){let r=g.join(t,e);if(d.existsSync(r))return r;let o=g.dirname(t);if(o===t)return null;t=o;}}function I(e=process.cwd()){return A(h,e)}function F(e=process.cwd()){let n=A(b,e);return n?g.dirname(n):null}function T(e){return !!(e.apiToken&&e.job&&Array.isArray(e.modes)&&e.modes.length>0)}async function P(e){let n=await d.readFile(e,"utf-8"),t=B.load(n);if(!t||typeof t!="object")throw new Error(`Invalid config in ${h}: expected a YAML object`);return t}async function L(e){let n=g.join(e,b);if(!await d.pathExists(n))return {};let t=await d.readFile(n,"utf-8"),r=JSON.parse(t),o=r[x]??r[N];if(o==null)return {};if(typeof o!="object"||Array.isArray(o))throw new Error(`Invalid ${b} config: "${x}" must be an object`);return o}async function U(){let e=process.cwd(),n=F(e)??e,t=g.join(n,h),r={},o=g.dirname(n),s=I(o);s&&await d.pathExists(s)&&(r=await P(s));let i=await L(n);r={...r,...i};let a=await d.pathExists(t)?await P(t):{};if(r={...r,...a},!T(r))throw new Error(`Config incomplete or not found: tried ${h} in project root, "${x}" in $
12
+ {PACKAGE_JSON}, and walking up from cwd. Required: apiToken, job, modes (non-empty array)`);return {...r,projectRoot:n}}var J=promisify(exec);async function M(){try{let{stdout:e}=await J("git branch --show-current"),n=e.trim();if(!n)throw new Error("Not on any branch (detached HEAD state)");return n}catch{throw new Error("Failed to get current branch. Are you in a git repository?")}}async function D(){try{let{stdout:e}=await J('git branch -r --format="%(refname:short)"');return e.split(`
13
+ `).filter(n=>n.includes("/")&&!n.includes("HEAD"))}catch{throw new Error("Failed to list branches. Are you in a git repository?")}}var $={maxRedirects:0,validateStatus:e=>e>=200&&e<400};function k(e){return `job/${e.split("/").map(t=>t.trim()).filter(Boolean).map(encodeURIComponent).join("/job/")}`}function H(e){let n=Array.isArray(e)?e.find(o=>o?.parameters):void 0,t=Array.isArray(n?.parameters)?n.parameters:[],r={};for(let o of t)o?.name&&(r[String(o.name)]=o?.value);return r}function _(e,n=400){try{let r=(typeof e=="string"?e:JSON.stringify(e??"",null,2)).trim();return r?r.length>n?`${r.slice(0,n)}...`:r:""}catch{return ""}}function V(e){let n=e.match(/^(https?):\/\/([^:]+):([^@]+)@(.+)$/);if(!n)throw new Error("Invalid apiToken format. Expected: http(s)://username:token@host:port");let[,t,r,o,s]=n,i=`${t}://${s.replace(/\/$/,"")}/`,a=K.create({baseURL:i,auth:{username:r,password:o},proxy:!1,timeout:3e4});return {baseURL:i,auth:{username:r,password:o},axios:a,crumbForm:void 0}}async function G(e,n){let r=(await e.axios.get("queue/api/json?tree=items[id,task[name],actions[parameters[name,value]],why]")).data.items;if(n){let o=n.trim(),s=o.split("/").filter(Boolean).pop();return r.filter(i=>{let a=i.task?.name;return a===o||(s?a===s:!1)})}return r}async function Y(e,n){let t=await e.axios.get(`${k(n)}/api/json?tree=builds[number,url,building,result,timestamp,duration,estimatedDuration,actions[parameters[name,value]]]`);return (Array.isArray(t.data?.builds)?t.data.builds:[]).filter(o=>o?.building).map(o=>({...o,parameters:H(o.actions)}))}async function Q(e,n,t){try{await e.axios.post(`${k(n)}/${t}/stop/`,new URLSearchParams({}),$);}catch(r){let o=r.response?.status;if(o===403){let s=typeof r.response?.data=="string"?r.response.data.slice(0,200):JSON.stringify(r.response?.data??"").slice(0,200),i=/crumb|csrf/i.test(s);console.log(R.red("[Error] 403 Forbidden: Jenkins \u62D2\u7EDD\u505C\u6B62\u6784\u5EFA\u8BF7\u6C42\uFF08\u53EF\u80FD\u662F\u6743\u9650\u6216 CSRF\uFF09\u3002")),console.log(R.yellow('[Hint] \u8BF7\u786E\u8BA4\u5F53\u524D\u7528\u6237\u5BF9\u8BE5 Job \u62E5\u6709 "Job" -> "Cancel" \u6743\u9650\u3002')),console.log(i?"[Hint] Jenkins returned a crumb/CSRF error. Ensure crumb + session cookie are sent, or use API token (not password).":s?`[Hint] Jenkins response: ${s}`:'[Hint] If "Job/Cancel" is already granted, also try granting "Run" -> "Update" (some versions use it for stop).');return}if(o===404||o===400||o===500){console.log(`[Info] Build #${t} could not be stopped (HTTP ${o}). Assuming it finished.`);return}throw r}}async function z(e,n,t){return W(e,n,{branch:t.branch,mode:t.mode})}async function W(e,n,t){let r="branch",o="mode",s=t[o];s&&console.log(`Checking for conflicting builds for mode: ${s}...`);let[i,a]=await Promise.all([G(e,n),Y(e,n)]),u=(c,l)=>{let f=c.actions?.find(w=>w.parameters);return f?f.parameters.find(w=>w.name===l)?.value:void 0},y=t[r];if(s&&y){let c=i.find(l=>u(l,o)===s&&u(l,r)===y);if(c)return console.log(`Build already in queue (ID: ${c.id}). Skipping trigger.`),new URL(`queue/item/${c.id}/`,e.baseURL).toString()}let p=!1;if(s)for(let c of a)u(c,o)===s&&(console.log(`Stopping running build #${c.number} (mode=${s})...`),await Q(e,n,c.number),p=!0);p&&await new Promise(c=>setTimeout(c,1e3)),s&&console.log(`Triggering new build for mode=${s}...`);try{return (await e.axios.post(`${k(n)}/buildWithParameters/`,new URLSearchParams({...t}),$)).headers.location||""}catch(c){let l=c?.response?.status,f=_(c?.response?.data);if(l===400){let m=[];m.push("Hint: Jenkins returned 400. Common causes: job is not parameterized, parameter names do not match, or the job endpoint differs (e.g. multibranch jobs)."),m.push(`Hint: This CLI sends parameters "${r}" and "${o}". Ensure your Jenkins Job has matching parameter names.`);let w=f?`
14
+ Jenkins response (snippet):
15
+ ${f}`:"";throw new Error(`Request failed with status code 400.${w}
16
+ ${m.join(`
17
+ `)}`)}if(l){let m=f?`
18
+ Jenkins response (snippet):
19
+ ${f}`:"";throw new Error(`Request failed with status code ${l}.${m}`)}throw c}}var ne=Z(fileURLToPath(import.meta.url),{interopDefault:!0});async function C(e){return await Promise.resolve(e)}function te(e){let n=String(e??"").trim();if(!n)return null;let t=n.split(":");if(t.length<2)return null;if(t.length===2){let[i,a]=t;return !i||!a?null:{file:i,kind:"Var",exportName:a}}let r=t.at(-2),o=t.at(-1);if(r!=="Fun"&&r!=="Var"||!o)return null;let s=t.slice(0,-2).join(":");return s?{file:s,kind:r,exportName:o}:null}function re(e,n){return g.isAbsolute(n)?n:g.join(e,n)}function j(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}async function S(e,n){let t=te(n);if(!t)return null;let r=re(e,t.file);if(!await d.pathExists(r))throw new Error(`configs reference not found: ${t.file}`);let o=ne(r),s=j(o)?o:{default:o},i=t.exportName==="default"?s.default:s[t.exportName];if(i===void 0)throw new Error(`configs reference export not found: ${n}`);return {kind:t.kind,value:i}}async function v(e,n){if(typeof n!="string")return n;let t=await S(e,n);if(!t)return n;if(t.kind==="Var"){if(typeof t.value=="function"){let o=t.value();return await C(o)}return await C(t.value)}if(typeof t.value!="function")throw new Error(`configs reference expected a function: ${n}`);let r=t.value();return await C(r)}async function oe(e,n){let t=Array.isArray(e)?e:[];if(t.length===0)return [];let r=String(n.projectRoot??"").trim()||process.cwd(),o=[];for(let s of t){if(!j(s))continue;let i={...s};if(i.choices!==void 0){i.choices=await v(r,i.choices);let a=String(i.type??"").toLowerCase(),u=i.choices;if((a==="list"||a==="rawlist"||a==="checkbox")&&typeof u!="function"&&!Array.isArray(u))if(u==null)i.choices=[];else if(typeof u=="string"||typeof u=="number"||typeof u=="boolean")i.choices=[String(u)];else if(typeof u[Symbol.iterator]=="function")i.choices=Array.from(u).map(p=>String(p));else throw new Error(`configs "${String(i.name??"")}" choices must resolve to an array (got ${typeof u})`)}if(i.default!==void 0&&(i.default=await v(r,i.default)),i.validate!==void 0){let a=i.validate;if(typeof a=="string"){let u=await S(r,a);if(!u)i.validate=a;else if(u.kind==="Var"){if(typeof u.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=u.value;}else {if(typeof u.value!="function")throw new Error(`validate reference must be a function: ${a}`);i.validate=(y,p)=>u.value(y,p);}}else typeof a=="function"&&(i.validate=a);}o.push(i);}return o}function se(e,n){let t=new Set(n),r={};for(let[o,s]of Object.entries(e))if(!t.has(o)&&s!==void 0){if(s===null){r[o]="";continue}if(Array.isArray(s)){r[o]=s.map(i=>String(i)).join(",");continue}if(typeof s=="object"){try{r[o]=JSON.stringify(s);}catch{r[o]=String(s);}continue}r[o]=String(s);}return r}
15
20
 
16
- export { j as createJenkinsClient, A as getAllBranches, b as getCurrentBranch, P as loadConfig, F as triggerBuild };
21
+ export { se as answersToJenkinsParams, V as createJenkinsClient, D as getAllBranches, M as getCurrentBranch, U as loadConfig, oe as resolveExtraQuestions, z as triggerBuild };
Binary file
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@ts-org/jenkins-cli",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Jenkins deployment CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
8
- "jenkins-cli": "./dist/cli.js"
8
+ "jenkins-cli": "dist/cli.js",
9
+ "jc": "dist/cli.js"
9
10
  },
10
11
  "scripts": {
11
12
  "build": "tsup",
12
13
  "dev": "NODE_ENV=development tsup --watch",
13
14
  "prepublishOnly": "npm run build",
14
- "link:test": "npm run build && npm link"
15
+ "link:test": "npm run build && npm link",
16
+ "format": "prettier . --write",
17
+ "format:check": "prettier . --check",
18
+ "lint": "eslint .",
19
+ "lint:fix": "eslint . --fix"
15
20
  },
16
21
  "keywords": [
17
22
  "jenkins",
@@ -19,7 +24,7 @@
19
24
  "deployment",
20
25
  "automation"
21
26
  ],
22
- "author": "Lynn<zmzchn@gmail.com>",
27
+ "author": "Lynn <zmzchn@gmail.com>",
23
28
  "license": "MIT",
24
29
  "repository": {
25
30
  "type": "git",
@@ -29,29 +34,41 @@
29
34
  "files": [
30
35
  "dist",
31
36
  "README.md",
32
- "LICENSE"
37
+ "LICENSE",
38
+ "docs/images/demo.png"
33
39
  ],
34
40
  "dependencies": {
35
- "axios": "^1.6.7",
36
- "chalk": "^4.1.2",
41
+ "axios": "^1.13.4",
42
+ "chalk": "^5.6.2",
37
43
  "commander": "11.1.0",
38
- "fs-extra": "^11.2.0",
39
- "inquirer": "8.2.7",
44
+ "fast-xml-parser": "^5.3.4",
45
+ "fs-extra": "^11.3.3",
46
+ "inquirer": "9.2.13",
47
+ "jiti": "^2.5.1",
40
48
  "js-yaml": "^4.1.1",
41
- "ora": "^5.4.1"
49
+ "ora": "^7.0.1"
42
50
  },
43
51
  "devDependencies": {
44
52
  "@types/fs-extra": "^11.0.4",
45
- "@types/inquirer": "^8.2.10",
53
+ "@types/inquirer": "^9.0.9",
46
54
  "@types/js-yaml": "^4.0.9",
47
- "@types/node": "^16.18.119",
55
+ "@types/node": "^16.18.126",
56
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
57
+ "@typescript-eslint/parser": "^6.21.0",
58
+ "eslint": "^8.57.0",
59
+ "eslint-config-prettier": "^9.1.0",
60
+ "prettier": "^3.2.5",
48
61
  "tsup": "^6.7.0",
49
- "typescript": "^5.3.3"
62
+ "typescript": "5.3.3"
50
63
  },
51
64
  "engines": {
52
65
  "node": ">=16"
53
66
  },
54
67
  "publishConfig": {
55
68
  "access": "public"
56
- }
57
- }
69
+ },
70
+ "directories": {
71
+ "doc": "docs"
72
+ },
73
+ "types": "./dist/index.d.ts"
74
+ }