@raolin2025/claude-code-node 2.7.0 → 2.7.2

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,371 @@
1
+ /**
2
+ * NpmPublish - npm 发布一键工具
3
+ *
4
+ * 封装了本项目 npm 发布的全部经验(见 NPM_STAGED_PUBLISH_GUIDE.md):
5
+ * - 标准 `npm publish` 常因 npm CLI 强制 otplease/2FA 返回 403
6
+ * - 最有效方法:bypass-2FA GAT token + 手动构造 PUT 请求 + `npm-auth-type: bearer`,无需 OTP
7
+ *
8
+ * 功能:
9
+ * - version 升版本号(patch/minor/major 或指定版本)
10
+ * - status 检查登录 / 当前版本 / registry 版本 / 暂存区状态
11
+ * - pack 打包 tarball
12
+ * - publish 完整发布:打包 + git 合并提交 + push + npm 发布(自动降级兜底)
13
+ * - manual-publish 仅 npm 发布(bypass token 手动 PUT 兜底)
14
+ */
15
+ import { execSync } from 'child_process'
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
17
+ import { createHash } from 'crypto'
18
+ import { join } from 'path'
19
+ import { ToolDef } from '../types/index.js'
20
+
21
+ const TOOL_NAME = 'NpmPublish'
22
+ const TOOL_DESCRIPTION = `
23
+ npm 发布一键工具 — 封装本项目发布经验,无需四处查找方法。
24
+
25
+ 前置条件:
26
+ - 项目根目录(含 package.json)
27
+ - ~/.npmrc 中配置了 bypass-2FA 的 _authToken(npm_... 前缀)
28
+ - git 已配置 user.name / user.email
29
+
30
+ 操作:
31
+ - status 检查登录 / 当前版本 / registry 版本 / 暂存区
32
+ - version 升版本号(如 2.7.0 -> 2.7.1),参数 version 可为 patch|minor|major 或具体版本号
33
+ - pack 打包 tarball
34
+ - publish 完整发布流程:升版本 + 打包 + git 合并提交(可选) + push + npm 发布
35
+ - manual-publish 仅做 npm 发布(标准 publish 失败时自动用 bypass token 手动 PUT 兜底,无需 OTP)
36
+
37
+ 关键经验:
38
+ - 标准 npm publish 常因 npm CLI 强制 2FA(otplease) 返回 403
39
+ - 有效兜底: 手动 PUT + npm-auth-type:bearer + bypass token,无需 OTP
40
+ - git 提交可合并为单个 release 提交(squash)
41
+ `
42
+ const TOOL_PARAMETERS = {
43
+ type: 'object',
44
+ properties: {
45
+ action: {
46
+ type: 'string',
47
+ enum: ['status', 'version', 'pack', 'publish', 'manual-publish'],
48
+ description: '要执行的操作'
49
+ },
50
+ version: {
51
+ type: 'string',
52
+ description: '版本增量(patch|minor|major)或具体版本号(如 2.7.1)。version/publish 使用'
53
+ },
54
+ commitMessage: {
55
+ type: 'string',
56
+ description: 'release 提交信息标题(publish 使用,默认 "release: v<version>")'
57
+ },
58
+ squash: {
59
+ type: 'boolean',
60
+ description: 'publish 时是否把多个提交合并为单个 release 提交(默认 true)',
61
+ default: true
62
+ },
63
+ doGitPush: {
64
+ type: 'boolean',
65
+ description: 'publish 时是否执行 git push(默认 true)',
66
+ default: true
67
+ },
68
+ doNpmPublish: {
69
+ type: 'boolean',
70
+ description: 'publish 时是否执行 npm 发布(默认 true)',
71
+ default: true
72
+ },
73
+ changelog: {
74
+ type: 'string',
75
+ description: '可选的 CHANGELOG 新增内容,publish 时会写入 CHANGELOG 顶部(可选)'
76
+ },
77
+ cwd: {
78
+ type: 'string',
79
+ description: '项目目录(默认当前目录)'
80
+ }
81
+ },
82
+ required: ['action']
83
+ }
84
+
85
+ /** 执行 shell 命令并返回输出(失败抛错) */
86
+ function sh(cmd, cwd) {
87
+ return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
88
+ }
89
+
90
+ /** 读取 ~/.npmrc 中的 token */
91
+ function getNpmToken() {
92
+ try {
93
+ const npmrc = readFileSync(join(process.env.HOME, '.npmrc'), 'utf-8')
94
+ const m = npmrc.match(/_authToken=([^\s]+)/)
95
+ return m ? m[1] : null
96
+ } catch { return null }
97
+ }
98
+
99
+ /** 读取 package.json */
100
+ function readPackage(cwd) {
101
+ return JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'))
102
+ }
103
+
104
+ /** 查询 registry packument */
105
+ async function fetchRegistry(name) {
106
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}`)
107
+ if (!res.ok) return null
108
+ return res.json()
109
+ }
110
+
111
+ /** 用 bypass token 手动 PUT 发布(核心兜底方法,无需 OTP) */
112
+ async function manualPublishDirect(cwd, tarballPath, token) {
113
+ const tarball = readFileSync(tarballPath)
114
+ const filename = tarballPath.split('/').pop()
115
+ const pkg = readPackage(cwd)
116
+ const shasum = createHash('sha1').update(tarball).digest('hex')
117
+ const integrity = 'sha512-' + createHash('sha512').update(tarball).digest('base64')
118
+ const encodedName = encodeURIComponent(pkg.name)
119
+ const regUrl = `https://registry.npmjs.org/${encodedName}`
120
+
121
+ // 1. 获取现有 packument
122
+ const getRes = await fetch(regUrl, { headers: { authorization: `Bearer ${token}` } })
123
+ const existing = await getRes.json()
124
+ const existingVersions = existing?.versions || {}
125
+ if (existingVersions[pkg.version]) {
126
+ return { ok: false, error: `版本 ${pkg.version} 已存在于 registry` }
127
+ }
128
+
129
+ // 2. 构造新版本 manifest 并合并
130
+ const versionManifest = {
131
+ ...pkg,
132
+ _id: `${pkg.name}@${pkg.version}`,
133
+ dist: {
134
+ shasum,
135
+ integrity,
136
+ tarball: `https://registry.npmjs.org/${pkg.name}/-/${filename}`,
137
+ fileCount: 61,
138
+ unpackedSize: tarball.length
139
+ }
140
+ }
141
+ const newDoc = {
142
+ ...existing,
143
+ _id: pkg.name,
144
+ name: pkg.name,
145
+ 'dist-tags': { ...(existing?.['dist-tags'] || {}), latest: pkg.version },
146
+ versions: { ...existingVersions, [pkg.version]: versionManifest },
147
+ _attachments: {
148
+ [filename]: { content_type: 'application/octet-stream', data: tarball.toString('base64'), length: tarball.length }
149
+ }
150
+ }
151
+
152
+ // 3. PUT 发布
153
+ const res = await fetch(regUrl, {
154
+ method: 'PUT',
155
+ headers: {
156
+ authorization: `Bearer ${token}`,
157
+ 'content-type': 'application/json',
158
+ 'npm-auth-type': 'bearer', // 关键
159
+ 'npm-command': 'publish'
160
+ },
161
+ body: JSON.stringify(newDoc)
162
+ })
163
+ const body = await res.text()
164
+ return { ok: res.ok, status: res.status, body: body.slice(0, 300) }
165
+ }
166
+
167
+ /** status: 检查发布环境 */
168
+ async function status(cwd) {
169
+ const lines = []
170
+ // 登录
171
+ try { lines.push(`npm whoami: ${sh('npm whoami', cwd)}`) }
172
+ catch { lines.push('npm whoami: 未登录 ⚠️') }
173
+ // token
174
+ const token = getNpmToken()
175
+ lines.push(token ? `~/.npmrc token: npm_${token.slice(4, 6)}... (${token.startsWith('npm_') ? 'GAT' : '未知类型'})` : '~/.npmrc token: 未找到 ⚠️')
176
+ // 本地版本
177
+ const pkg = readPackage(cwd)
178
+ lines.push(`本地版本: ${pkg.version}`)
179
+ // registry 版本
180
+ const reg = await fetchRegistry(pkg.name)
181
+ if (reg) {
182
+ lines.push(`registry latest: ${reg['dist-tags']?.latest}`)
183
+ lines.push(`registry 是否含本地版本: ${reg.versions?.[pkg.version] ? '是(已发布)' : '否(待发布)'}`)
184
+ } else {
185
+ lines.push('registry: 查询失败(网络/权限)')
186
+ }
187
+ // git 状态
188
+ try {
189
+ const ahead = sh('git rev-list --count @{u}..HEAD 2>/dev/null || echo 0', cwd)
190
+ lines.push(`git: 领先远程 ${ahead} 个提交`)
191
+ const dirty = sh('git status --porcelain', cwd)
192
+ lines.push(dirty ? `工作区有未提交改动:\n${dirty}` : '工作区干净')
193
+ } catch { lines.push('git: 非 git 仓库') }
194
+ return lines.join('\n')
195
+ }
196
+
197
+ /** version: 升版本号 */
198
+ async function bumpVersion(cwd, version) {
199
+ const inc = ['patch', 'minor', 'major'].includes(version) ? version : null
200
+ const cmd = inc
201
+ ? `npm version ${inc} --no-git-tag-version`
202
+ : `npm version ${version} --no-git-tag-version`
203
+ try {
204
+ const out = sh(cmd, cwd)
205
+ return `版本已更新 → ${readPackage(cwd).version}\n${out}`
206
+ } catch (e) {
207
+ throw new Error(`升版本失败: ${e.message}`)
208
+ }
209
+ }
210
+
211
+ /** pack: 打包 */
212
+ async function pack(cwd) {
213
+ const out = sh('npm pack --json', cwd)
214
+ let filename = ''
215
+ try {
216
+ const arr = JSON.parse(out)
217
+ filename = arr[0]?.filename || ''
218
+ } catch {
219
+ const m = out.match(/([^\s]+\.tgz)/)
220
+ filename = m ? m[1] : ''
221
+ }
222
+ const pkg = readPackage(cwd)
223
+ return `打包完成: ${filename}\n版本: ${pkg.version}`
224
+ }
225
+
226
+ /** publish: 完整发布流程 */
227
+ async function doPublish({ cwd, version, commitMessage, squash, doGitPush, doNpmPublish, changelog }) {
228
+ const steps = []
229
+ // 0. 前置检查
230
+ const token = getNpmToken()
231
+ if (doNpmPublish && !token) throw new Error('未找到 ~/.npmrc 的 _authToken,无法 npm 发布')
232
+ let pkg = readPackage(cwd)
233
+ const currentVersion = pkg.version
234
+
235
+ // 1. 升版本
236
+ if (version) {
237
+ try {
238
+ const out = await bumpVersion(cwd, version)
239
+ steps.push(out)
240
+ pkg = readPackage(cwd)
241
+ } catch (e) {
242
+ return { ok: false, steps, error: `升版本失败: ${e.message}` }
243
+ }
244
+ }
245
+
246
+ // 2. 写 CHANGELOG(可选)
247
+ if (changelog) {
248
+ const clPath = join(cwd, 'CHANGELOG.md')
249
+ const head = `## v${pkg.version}\n\n${changelog.trim()}\n\n`
250
+ if (existsSync(clPath)) {
251
+ const orig = readFileSync(clPath, 'utf-8')
252
+ // 在 "# CHANGELOG" 标题后插入
253
+ const idx = orig.indexOf('\n')
254
+ writeFileSync(clPath, orig.slice(0, idx + 1) + '\n' + head + orig.slice(idx + 1))
255
+ } else {
256
+ writeFileSync(clPath, `# CHANGELOG\n\n${head}`)
257
+ }
258
+ steps.push('CHANGELOG 已更新')
259
+ }
260
+
261
+ // 3. 打包
262
+ let tarballPath = ''
263
+ try {
264
+ tarballPath = join(cwd, sh('npm pack --json', cwd).match(/"filename":"([^"]+)"/)?.[1] || '')
265
+ steps.push(`打包: ${tarballPath.split('/').pop()}`)
266
+ } catch (e) {
267
+ return { ok: false, steps, error: `打包失败: ${e.message}` }
268
+ }
269
+
270
+ // 4. git 提交(合并或直接提交)
271
+ const msg = commitMessage || `release: v${pkg.version}`
272
+ try {
273
+ if (squash !== false) {
274
+ // 合并为单个 release 提交:soft reset 到上一个 release 提交基点,再一次性提交
275
+ // 基点 = HEAD 之前最近的一个 "release:" 提交(不含当前),若没有则用 HEAD~n 之前全部
276
+ let baseCommit = null
277
+ try {
278
+ // 最近的两个 release 提交中,取最早那个作为基点(即当前 release 之前的状态)
279
+ const releases = sh('git log --format="%h" --grep="^release:"', cwd).split('\n').filter(Boolean)
280
+ // releases[0] 是最近的 release(可能是本次或上一次);若 HEAD 就是 release 则取 [1]
281
+ const headIsRelease = sh('git log -1 --format="%s"', cwd).startsWith('release:')
282
+ baseCommit = headIsRelease ? (releases[1] || releases[0]) : (releases[0] || 'HEAD')
283
+ } catch { baseCommit = 'HEAD' }
284
+ sh(`git reset --soft ${baseCommit}`, cwd)
285
+ }
286
+ sh(`git add -A`, cwd)
287
+ sh(`git commit -m "${msg.replace(/"/g, '\\"')}"`, cwd)
288
+ steps.push(`git 提交: ${msg}`)
289
+ } catch (e) {
290
+ if (e.message.includes('nothing to commit') || e.message.includes('没有') || e.message.includes('nothing added')) {
291
+ steps.push('git: 无改动可提交')
292
+ } else {
293
+ steps.push(`git 提交失败: ${e.message}`)
294
+ }
295
+ }
296
+
297
+ // 5. git push
298
+ if (doGitPush !== false) {
299
+ try {
300
+ const out = sh('git push', cwd)
301
+ steps.push('git push: 成功')
302
+ } catch (e) {
303
+ steps.push(`git push 失败: ${e.message}`)
304
+ }
305
+ }
306
+
307
+ // 6. npm 发布
308
+ if (doNpmPublish !== false) {
309
+ // 先尝试标准 publish
310
+ try {
311
+ const out = sh(`npm publish "${tarballPath}" 2>&1`, cwd)
312
+ if (out.includes('+ @')) {
313
+ steps.push('npm publish: 成功(标准方式)')
314
+ } else {
315
+ steps.push('npm publish: 标准方式未直接成功,尝试 bypass 兜底...')
316
+ const r = await manualPublishDirect(cwd, tarballPath, token)
317
+ if (r.ok) steps.push(`npm publish: 成功(bypass token 手动 PUT, ${r.status})`)
318
+ else steps.push(`npm publish: 失败 - ${r.error || r.body}`)
319
+ }
320
+ } catch (e) {
321
+ steps.push('npm publish: 标准方式抛错,尝试 bypass 兜底...')
322
+ try {
323
+ const r = await manualPublishDirect(cwd, tarballPath, token)
324
+ if (r.ok) steps.push(`npm publish: 成功(bypass token 手动 PUT, ${r.status})`)
325
+ else steps.push(`npm publish: 失败 - ${r.error || r.body}`)
326
+ } catch (e2) {
327
+ steps.push(`npm publish: 失败 - ${e2.message}`)
328
+ }
329
+ }
330
+ }
331
+
332
+ return { ok: true, version: pkg.version, steps }
333
+ }
334
+
335
+ async function handler(input) {
336
+ const cwd = input.cwd || process.cwd()
337
+ if (!existsSync(join(cwd, 'package.json'))) {
338
+ return '错误: 当前目录没有 package.json,请通过 cwd 指定项目目录'
339
+ }
340
+ const action = input.action
341
+ try {
342
+ switch (action) {
343
+ case 'status': return await status(cwd)
344
+ case 'version':
345
+ if (!input.version) return '请提供 version 参数(patch|minor|major 或具体版本号)'
346
+ return await bumpVersion(cwd, input.version)
347
+ case 'pack': return await pack(cwd)
348
+ case 'publish': {
349
+ const r = await doPublish(input)
350
+ return `✅ npm 发布流程完成 (v${r.version})\n\n` + r.steps.map(s => `- ${s}`).join('\n') + (r.error ? `\n\n⚠️ 错误: ${r.error}` : '')
351
+ }
352
+ case 'manual-publish': {
353
+ const token = getNpmToken()
354
+ if (!token) return '错误: 未找到 ~/.npmrc 的 _authToken'
355
+ // 找最新的 tgz
356
+ const tgz = sh('ls -t *.tgz 2>/dev/null | head -1', cwd)
357
+ if (!tgz) return '错误: 目录下没有 .tgz 文件,请先执行 pack'
358
+ const tarballPath = join(cwd, tgz)
359
+ const r = await manualPublishDirect(cwd, tarballPath, token)
360
+ return r.ok ? `✅ 发布成功 (${r.status})\n${r.body}` : `❌ 发布失败: ${r.error || r.body}`
361
+ }
362
+ default: return `未知 action: ${action}`
363
+ }
364
+ } catch (e) {
365
+ return `❌ 工具执行错误: ${e.message}`
366
+ }
367
+ }
368
+
369
+ export const npmPublishTool = new ToolDef(TOOL_NAME, TOOL_DESCRIPTION, TOOL_PARAMETERS, handler, 'ask')
370
+
371
+ export default npmPublishTool