@andyqiu/teamkit 0.1.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.
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/capability-detect.mjs — teamkit 安装前能力探测
4
+ *
5
+ * 探测 5 个功能下限(ADR: opencode-version-compatibility §Decision §1):
6
+ * 1. plugin 加载机制(@opencode-ai/plugin 包存在 或 opencode.json plugins[] 可读)
7
+ * 2. ctx 上下文对象至少 1 个字段可访问
8
+ * 3. opencode 配置可读写(dry-run,不实际写)
9
+ * 4. AGENTS.md 全局路径可写(~/.config/opencode/AGENTS.md)
10
+ * 5. experimental.session.compacting hook 可被识别
11
+ *
12
+ * 退出码:
13
+ * 0 = 全部必需能力通过
14
+ * 1 = 任意必需能力失败
15
+ *
16
+ * ADR: opencode-version-compatibility
17
+ */
18
+
19
+ import { readFileSync, existsSync, constants as fsConstants } from "node:fs"
20
+ import { access } from "node:fs/promises"
21
+ import { join } from "node:path"
22
+ import { homedir } from "node:os"
23
+ import { execSync } from "node:child_process"
24
+ import { createRequire } from "node:module"
25
+ import process from "node:process"
26
+ import { fileURLToPath } from "node:url"
27
+
28
+ const HOME = homedir()
29
+ const OPENCODE_CONFIG_DIR = join(HOME, ".config", "opencode")
30
+ const OPENCODE_JSON_PATH = join(OPENCODE_CONFIG_DIR, "opencode.json")
31
+ const AGENTS_MD_PATH = join(OPENCODE_CONFIG_DIR, "AGENTS.md")
32
+ const require = createRequire(import.meta.url)
33
+
34
+ const results = {
35
+ pluginLoad: { ok: false, label: "plugin 加载机制(@opencode-ai/plugin 或 opencode.json plugins[])" },
36
+ ctxObject: { ok: false, label: "ctx 上下文对象可访问(@opencode-ai/plugin API)" },
37
+ configReadWrite: { ok: false, label: "opencode 配置可读写(~/.config/opencode/opencode.json)" },
38
+ agentsMd: { ok: false, label: "AGENTS.md 全局路径可写(~/.config/opencode/AGENTS.md)" },
39
+ sessionCompacting:{ ok: false, label: "experimental.session.compacting hook 可被识别" },
40
+ }
41
+
42
+ const warnings = []
43
+
44
+ // ─── 探测 1:plugin 加载机制 ─────────────────────────────────────────────────
45
+ function detectPluginLoad() {
46
+ // 优先:@opencode-ai/plugin 包可解析
47
+ try {
48
+ require.resolve("@opencode-ai/plugin")
49
+ results.pluginLoad.ok = true
50
+ return
51
+ } catch { /* 继续 fallback */ }
52
+
53
+ // Fallback 1:opencode.json 存在且有 plugin/plugins 数组
54
+ try {
55
+ if (existsSync(OPENCODE_JSON_PATH)) {
56
+ const raw = JSON.parse(readFileSync(OPENCODE_JSON_PATH, "utf8"))
57
+ if (Array.isArray(raw?.plugin) || Array.isArray(raw?.plugins)) {
58
+ results.pluginLoad.ok = true
59
+ return
60
+ }
61
+ }
62
+ } catch { /* 失败 */ }
63
+
64
+ // Fallback 2:opencode CLI 存在即乐观通过(新用户首装场景)
65
+ // install.sh 后续会写入 opencode.json,此时文件可能尚不存在
66
+ try {
67
+ execSync('opencode --version', { timeout: 5000, stdio: 'ignore' })
68
+ warnings.push("⚠️ plugin 加载机制:opencode 存在,乐观通过(首装场景)")
69
+ results.pluginLoad.ok = true
70
+ return
71
+ } catch { /* opencode 不存在 */ }
72
+ }
73
+
74
+ // ─── 探测 2:ctx 上下文对象 ──────────────────────────────────────────────────
75
+ function detectCtxObject() {
76
+ try {
77
+ const mod = require("@opencode-ai/plugin")
78
+ if (mod && typeof mod === "object" && Object.keys(mod).length > 0) {
79
+ results.ctxObject.ok = true
80
+ return
81
+ }
82
+ } catch { /* 包不存在 */ }
83
+
84
+ // Phase 1 MVP:包不存在时乐观通过,Phase 2 收紧
85
+ // ADR §Decision §3:Phase 1 不主动 probe IPC
86
+ warnings.push("⚠️ 可选说明:@opencode-ai/plugin 未找到,ctx 探测乐观通过(Phase 1 行为)")
87
+ results.ctxObject.ok = true
88
+ }
89
+
90
+ // ─── 探测 3:opencode 配置可读写 ─────────────────────────────────────────────
91
+ async function detectConfigReadWrite() {
92
+ // 逐层向上找可写祖先目录
93
+ const candidates = [
94
+ OPENCODE_CONFIG_DIR,
95
+ join(HOME, ".config"),
96
+ HOME,
97
+ ]
98
+ for (const dir of candidates) {
99
+ if (!existsSync(dir)) continue
100
+ try {
101
+ await access(dir, fsConstants.W_OK | fsConstants.R_OK)
102
+ results.configReadWrite.ok = true
103
+ return
104
+ } catch { /* 继续上一层 */ }
105
+ }
106
+ }
107
+
108
+ // ─── 探测 4:AGENTS.md 全局路径可写 ─────────────────────────────────────────
109
+ async function detectAgentsMd() {
110
+ if (existsSync(AGENTS_MD_PATH)) {
111
+ try {
112
+ await access(AGENTS_MD_PATH, fsConstants.W_OK)
113
+ results.agentsMd.ok = true
114
+ return
115
+ } catch { return }
116
+ }
117
+
118
+ // 文件不存在 → 检测父目录
119
+ const candidates = [
120
+ OPENCODE_CONFIG_DIR,
121
+ join(HOME, ".config"),
122
+ HOME,
123
+ ]
124
+ for (const dir of candidates) {
125
+ if (!existsSync(dir)) continue
126
+ try {
127
+ await access(dir, fsConstants.W_OK)
128
+ results.agentsMd.ok = true
129
+ return
130
+ } catch { /* 继续 */ }
131
+ }
132
+ }
133
+
134
+ // ─── 探测 5:experimental.session.compacting hook ────────────────────────────
135
+ // REQUIRED — hard fail(ADR: opencode-version-compatibility §Decision §4)
136
+ //
137
+ // 虚采纳-4 修正(Phase 1.2):
138
+ // 之前:依赖 @opencode-ai/plugin npm 包字段探测(在 file:// 部署模式下失效——
139
+ // opencode 通过 `plugin` key + file:// URI 加载插件,不通过 npm 包;
140
+ // 因此 @opencode-ai/plugin 不存在于 node_modules,任何正常安装环境都会 exit(1))
141
+ // probe plugin 方案局限:opencode run 无 --plugin CLI flag(仅有 --pure),
142
+ // 无法在 capability-detect 中用 `opencode run --pure --plugin file://... "probe"` 单次运行;
143
+ // probe plugin 仍作为 T4/T5 验收测试 fixture 保留(tests/fixtures/probe-compacting-plugin.js)
144
+ // 现在:版本检查方案——opencode >= 1.x 均支持 experimental.session.compacting;
145
+ // 通过 `opencode --version` 判断主版本号,>= 1 则通过
146
+ async function detectSessionCompacting() {
147
+ // 版本检查:opencode >= 1.x 均支持 session.compacting(ADR: opencode-version-compatibility)
148
+ try {
149
+ const ver = execSync("opencode --version", { timeout: 5000 }).toString().trim()
150
+ const major = parseInt(ver.split(".")[0], 10)
151
+ if (!isNaN(major) && major >= 1) {
152
+ results.sessionCompacting.ok = true
153
+ return
154
+ }
155
+ // 版本号 < 1(如 0.x)→ hard fail
156
+ results.sessionCompacting.ok = false
157
+ return
158
+ } catch {
159
+ // opencode CLI 不可用 → 无法确认 hook 支持 → hard fail
160
+ results.sessionCompacting.ok = false
161
+ }
162
+ }
163
+
164
+ // ─── 可选能力探测 ─────────────────────────────────────────────────────────────
165
+ function detectOptionalCapabilities() {
166
+ try {
167
+ const mod = require("@opencode-ai/plugin")
168
+
169
+ if (!mod?.workingMemory && !mod?.WorkingMemory && !mod?.working_memory) {
170
+ warnings.push("⚠️ 可选能力缺失:working_memory API(M1 KH 注入将降级)")
171
+ }
172
+
173
+ const hookNames = mod?.HOOK_NAMES ?? mod?.SUPPORTED_HOOKS ?? mod?.hookNames ?? []
174
+ if (Array.isArray(hookNames) && !hookNames.includes("session.end")) {
175
+ warnings.push("⚠️ 可选能力缺失:session.end hook(auto-learning 将降级)")
176
+ }
177
+ } catch {
178
+ // @opencode-ai/plugin 不存在,跳过可选探测
179
+ }
180
+ }
181
+
182
+ // ─── 主流程 ───────────────────────────────────────────────────────────────────
183
+ async function main() {
184
+ process.stdout.write("teamkit capability-detect: 探测 OpenCode 功能下限...\n")
185
+
186
+ detectPluginLoad()
187
+ detectCtxObject()
188
+ await detectSessionCompacting()
189
+ detectOptionalCapabilities()
190
+ await detectConfigReadWrite()
191
+ await detectAgentsMd()
192
+
193
+ for (const w of warnings) {
194
+ console.warn(w)
195
+ }
196
+
197
+ const failures = Object.entries(results).filter(([, v]) => !v.ok)
198
+
199
+ if (failures.length === 0) {
200
+ process.stdout.write("✅ 全部 5 项功能下限探测通过\n")
201
+ process.exit(0)
202
+ }
203
+
204
+ process.stderr.write("\n❌ teamkit: 以下必需能力不可用:\n\n")
205
+
206
+ for (const [key, v] of failures) {
207
+ process.stderr.write(` ✗ ${v.label}\n`)
208
+
209
+ if (key === "sessionCompacting") {
210
+ // ADR: opencode-version-compatibility §Decision §4
211
+ process.stderr.write(
212
+ "\n" +
213
+ " ⚠️ experimental.session.compacting hook 不可用!\n" +
214
+ " 此 hook 是 teamkit M2 层(长对话 KH 保护)的核心依赖。\n" +
215
+ " 请升级 OpenCode 至支持 experimental.session.compacting hook 的版本。\n" +
216
+ " 参考:docs/compatibility.md\n\n"
217
+ )
218
+ }
219
+ }
220
+
221
+ process.stderr.write(
222
+ "install 已中止。请修复上述问题后重新运行 install.sh\n" +
223
+ "运行 `teamkit doctor` 获取详细诊断信息\n"
224
+ )
225
+ process.exit(1)
226
+ }
227
+
228
+ main().catch((err) => {
229
+ process.stderr.write(`capability-detect 内部错误: ${err.message}\n`)
230
+ process.exit(1)
231
+ })
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/merge-agents-md.mjs — install.sh / install.ps1 共用的 AGENTS.md 智能合并 CLI
4
+ *
5
+ * 从 code-forge/scripts/merge-agents-md.mjs 搬迁,适配 teamkit。
6
+ * 改动:注释将 CodeForge → teamkit,其余逻辑不变。
7
+ *
8
+ * 算法语义与 lib/agents-merge.ts 严格一致(单一来源)。
9
+ * 自带实现而不是 import lib/agents-merge.ts,因为 install 时 lib/ 可能不可访问。
10
+ *
11
+ * 用法:
12
+ * node scripts/merge-agents-md.mjs --target <path/to/AGENTS.md> --template <path/to/kh-instructions.md>
13
+ *
14
+ * 选项:
15
+ * --target <path> 目标 AGENTS.md 路径(必填)
16
+ * --template <path> KH instructions 模板路径(必填)
17
+ * --dry-run 仅打印将要做什么,不实际写文件
18
+ * --no-backup 跳过 .bak 备份(默认会备份)
19
+ * --quiet 静默模式(仅错误输出)
20
+ * -h / --help 帮助
21
+ *
22
+ * 退出码:
23
+ * 0 = 成功(或 dry-run)
24
+ * 1 = 参数错误
25
+ * 2 = 模板文件不存在
26
+ * 3 = 写文件失败
27
+ */
28
+
29
+ import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"
30
+ import { basename } from "node:path"
31
+ import process from "node:process"
32
+
33
+ const KH_MARKER_START = "<!-- knowledge-hub:start -->"
34
+ const KH_MARKER_END = "<!-- knowledge-hub:end -->"
35
+ const START_PREFIX = "<!-- knowledge-hub:start"
36
+ const END_PREFIX = "<!-- knowledge-hub:end"
37
+
38
+ // ────────────────────────────────────────────────────────────────────
39
+ // 算法:与 lib/agents-merge.ts 一致
40
+ // ────────────────────────────────────────────────────────────────────
41
+
42
+ function findMarkerLine(text, prefix, fromLine = 0) {
43
+ const lines = text.split("\n")
44
+ for (let i = fromLine; i < lines.length; i++) {
45
+ if (lines[i].trimStart().startsWith(prefix)) return i
46
+ }
47
+ return -1
48
+ }
49
+
50
+ function normalizeTemplate(tpl) {
51
+ return tpl.replace(/\n+$/g, "")
52
+ }
53
+
54
+ function backupSuffix(now = new Date()) {
55
+ const pad = (n) => String(n).padStart(2, "0")
56
+ return (
57
+ `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` +
58
+ `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
59
+ )
60
+ }
61
+
62
+ function backupPath(originalPath, now = new Date()) {
63
+ return `${originalPath}.bak.${backupSuffix(now)}`
64
+ }
65
+
66
+ function createDefault(template) {
67
+ const tpl = normalizeTemplate(template)
68
+ return [
69
+ "# 项目 AGENTS.md",
70
+ "",
71
+ KH_MARKER_START,
72
+ "",
73
+ tpl,
74
+ "",
75
+ KH_MARKER_END,
76
+ "",
77
+ "## 项目专属约定",
78
+ "",
79
+ "(在此添加本项目特有的开发约定)",
80
+ "",
81
+ ].join("\n")
82
+ }
83
+
84
+ function mergeAgentsMd({ template, existing }) {
85
+ const tpl = normalizeTemplate(template)
86
+
87
+ if (existing == null || existing.length === 0) {
88
+ return { content: createDefault(tpl), mode: "created" }
89
+ }
90
+
91
+ const startIdx = findMarkerLine(existing, START_PREFIX)
92
+ const endIdx = startIdx === -1 ? -1 : findMarkerLine(existing, END_PREFIX, startIdx + 1)
93
+
94
+ if (startIdx !== -1 && endIdx !== -1) {
95
+ const lines = existing.split("\n")
96
+ const before = lines.slice(0, startIdx + 1)
97
+ const after = lines.slice(endIdx)
98
+ const middle = ["", tpl, ""]
99
+ return {
100
+ content: [...before, ...middle, ...after].join("\n"),
101
+ mode: "replaced",
102
+ }
103
+ }
104
+
105
+ const block = `${KH_MARKER_START}\n\n${tpl}\n\n${KH_MARKER_END}\n\n`
106
+ return {
107
+ content: block + existing,
108
+ mode: "inserted",
109
+ }
110
+ }
111
+
112
+ // ────────────────────────────────────────────────────────────────────
113
+ // CLI
114
+ // ────────────────────────────────────────────────────────────────────
115
+
116
+ const HELP = `
117
+ scripts/merge-agents-md.mjs — AGENTS.md 智能合并 CLI(teamkit)
118
+
119
+ 用法:
120
+ node scripts/merge-agents-md.mjs --target <path> --template <path> [选项]
121
+
122
+ 选项:
123
+ --target <path> 目标 AGENTS.md 路径(必填)
124
+ --template <path> KH instructions 模板路径(必填)
125
+ --dry-run 仅打印将要做什么
126
+ --no-backup 跳过 .bak 备份(默认会备份)
127
+ --quiet 静默模式
128
+ -h / --help 本帮助
129
+
130
+ 退出码:
131
+ 0 成功 1 参数错误 2 模板缺失 3 写文件失败
132
+ `
133
+
134
+ function parseArgs(argv) {
135
+ const args = { target: null, template: null, dryRun: false, backup: true, quiet: false }
136
+ for (let i = 0; i < argv.length; i++) {
137
+ const a = argv[i]
138
+ if (a === "-h" || a === "--help") {
139
+ process.stdout.write(HELP)
140
+ process.exit(0)
141
+ } else if (a === "--target") args.target = argv[++i]
142
+ else if (a === "--template") args.template = argv[++i]
143
+ else if (a === "--dry-run") args.dryRun = true
144
+ else if (a === "--no-backup") args.backup = false
145
+ else if (a === "--quiet") args.quiet = true
146
+ else {
147
+ process.stderr.write(`未知参数: ${a}\n${HELP}`)
148
+ process.exit(1)
149
+ }
150
+ }
151
+ return args
152
+ }
153
+
154
+ function main() {
155
+ const args = parseArgs(process.argv.slice(2))
156
+ if (!args.target || !args.template) {
157
+ process.stderr.write("错误:--target 和 --template 都必须指定\n" + HELP)
158
+ process.exit(1)
159
+ }
160
+
161
+ const log = args.quiet ? () => {} : (msg) => process.stdout.write(msg + "\n")
162
+
163
+ if (!existsSync(args.template)) {
164
+ process.stderr.write(`模板文件不存在: ${args.template}\n`)
165
+ process.exit(2)
166
+ }
167
+ const template = readFileSync(args.template, "utf8")
168
+ const existing = existsSync(args.target) ? readFileSync(args.target, "utf8") : null
169
+ const result = mergeAgentsMd({ template, existing })
170
+
171
+ if (args.dryRun) {
172
+ log(`[dry-run] mode=${result.mode} target=${args.target}`)
173
+ log(`[dry-run] 将写入 ${result.content.length} 字节`)
174
+ if (existing && args.backup) {
175
+ log(`[dry-run] 将备份到 ${backupPath(args.target)}`)
176
+ }
177
+ process.exit(0)
178
+ }
179
+
180
+ if (existing && args.backup) {
181
+ const bak = backupPath(args.target)
182
+ try {
183
+ copyFileSync(args.target, bak)
184
+ log(` ✓ 已备份原 AGENTS.md → ${basename(bak)}`)
185
+ } catch (err) {
186
+ process.stderr.write(`备份失败: ${err.message}\n`)
187
+ process.exit(3)
188
+ }
189
+ }
190
+
191
+ try {
192
+ writeFileSync(args.target, result.content, "utf8")
193
+ log(` ✓ AGENTS.md ${result.mode}: ${args.target}`)
194
+ process.exit(0)
195
+ } catch (err) {
196
+ process.stderr.write(`写入失败: ${err.message}\n`)
197
+ process.exit(3)
198
+ }
199
+ }
200
+
201
+ main()
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # publish.sh — teamkit 一键发布脚本
3
+ #
4
+ # 用法:
5
+ # bash scripts/publish.sh # 发布当前版本(不 bump)
6
+ # bash scripts/publish.sh --patch # bump patch 后发布
7
+ # bash scripts/publish.sh --minor # bump minor 后发布
8
+ # bash scripts/publish.sh --major # bump major 后发布
9
+ #
10
+ # 环境变量:
11
+ # NPM_OTP 若已设置,自动用于 NPM_CONFIG_OTP;否则脚本交互式询问
12
+ #
13
+ # 步骤:
14
+ # 1. 读取 / 展示当前版本
15
+ # 2. (可选)bump 版本
16
+ # 3. npm run build && npm run typecheck
17
+ # 4. git tag v<version>(只创建,不 push)
18
+ # 5. npm publish --access public
19
+ # 6. git push && git push --tags(publish 成功后才推送)
20
+ # 7. 成功提示
21
+
22
+ set -euo pipefail
23
+
24
+ # ────────────────────────────────────────────────────────────────────────────
25
+ # 颜色 & 输出工具(与 install.sh 一致)
26
+ # ────────────────────────────────────────────────────────────────────────────
27
+ RED='\033[0;31m'
28
+ YELLOW='\033[1;33m'
29
+ GREEN='\033[0;32m'
30
+ CYAN='\033[0;36m'
31
+ NC='\033[0m'
32
+
33
+ err() { printf "${RED}❌ %s${NC}\n" "$*" >&2; }
34
+ warn() { printf "${YELLOW}⚠️ %s${NC}\n" "$*"; }
35
+ info() { printf "${CYAN}ℹ️ %s${NC}\n" "$*"; }
36
+ ok() { printf "${GREEN}✅ %s${NC}\n" "$*"; }
37
+
38
+ # ────────────────────────────────────────────────────────────────────────────
39
+ # 定位项目根目录(脚本可从任意目录调用)
40
+ # ────────────────────────────────────────────────────────────────────────────
41
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
42
+ ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
43
+ PACKAGE_JSON="${ROOT_DIR}/package.json"
44
+
45
+ # ────────────────────────────────────────────────────────────────────────────
46
+ # 参数解析
47
+ # ────────────────────────────────────────────────────────────────────────────
48
+ BUMP=""
49
+
50
+ for arg in "$@"; do
51
+ case "$arg" in
52
+ --patch) BUMP="patch" ;;
53
+ --minor) BUMP="minor" ;;
54
+ --major) BUMP="major" ;;
55
+ *)
56
+ err "未知参数:$arg"
57
+ err "用法:$0 [--patch|--minor|--major]"
58
+ exit 1
59
+ ;;
60
+ esac
61
+ done
62
+
63
+ # ────────────────────────────────────────────────────────────────────────────
64
+ # 步骤 1:读取当前版本
65
+ # ────────────────────────────────────────────────────────────────────────────
66
+ if ! command -v node &>/dev/null; then
67
+ err "未找到 node,请先安装 Node.js"
68
+ exit 1
69
+ fi
70
+
71
+ CURRENT_VERSION="$(node -e "process.stdout.write(require('${PACKAGE_JSON}').version)")"
72
+ info "当前版本:${CURRENT_VERSION}"
73
+
74
+ # ────────────────────────────────────────────────────────────────────────────
75
+ # 步骤 2:(可选)bump 版本
76
+ # ────────────────────────────────────────────────────────────────────────────
77
+ bump_semver() {
78
+ local ver="$1"
79
+ local part="$2"
80
+ local major minor patch
81
+ IFS='.' read -r major minor patch <<< "$ver"
82
+ case "$part" in
83
+ major) major=$((major + 1)); minor=0; patch=0 ;;
84
+ minor) minor=$((minor + 1)); patch=0 ;;
85
+ patch) patch=$((patch + 1)) ;;
86
+ esac
87
+ echo "${major}.${minor}.${patch}"
88
+ }
89
+
90
+ if [[ -n "$BUMP" ]]; then
91
+ NEW_VERSION="$(bump_semver "$CURRENT_VERSION" "$BUMP")"
92
+ info "Bump ${BUMP}:${CURRENT_VERSION} → ${NEW_VERSION}"
93
+
94
+ # 用 node 原地更新 package.json(保留格式)
95
+ node - <<EOF
96
+ const fs = require('fs');
97
+ const pkg = JSON.parse(fs.readFileSync('${PACKAGE_JSON}', 'utf8'));
98
+ pkg.version = '${NEW_VERSION}';
99
+ fs.writeFileSync('${PACKAGE_JSON}', JSON.stringify(pkg, null, 2) + '\n');
100
+ EOF
101
+ ok "package.json 已更新版本为 ${NEW_VERSION}"
102
+ VERSION="$NEW_VERSION"
103
+ else
104
+ VERSION="$CURRENT_VERSION"
105
+ info "不 bump 版本,使用当前版本 ${VERSION}"
106
+ fi
107
+
108
+ # ────────────────────────────────────────────────────────────────────────────
109
+ # 用户确认
110
+ # ────────────────────────────────────────────────────────────────────────────
111
+ printf "\n"
112
+ warn "即将发布 @andyqiu/teamkit@${VERSION}"
113
+ read -r -p "$(printf "${YELLOW}确认发布?[y/N] ${NC}")" CONFIRM
114
+ case "$CONFIRM" in
115
+ [yY][eE][sS]|[yY]) ;;
116
+ *)
117
+ info "已取消发布"
118
+ exit 0
119
+ ;;
120
+ esac
121
+ printf "\n"
122
+
123
+ # ────────────────────────────────────────────────────────────────────────────
124
+ # 步骤 3:build + typecheck
125
+ # ────────────────────────────────────────────────────────────────────────────
126
+ info "正在执行 build + typecheck …"
127
+ (cd "$ROOT_DIR" && npm run build && npm run typecheck)
128
+ ok "build + typecheck 通过"
129
+
130
+ # ────────────────────────────────────────────────────────────────────────────
131
+ # 步骤 4:git tag(只创建,不 push)
132
+ # ────────────────────────────────────────────────────────────────────────────
133
+ TAG="v${VERSION}"
134
+ info "创建 git tag ${TAG} …"
135
+
136
+ (
137
+ cd "$ROOT_DIR"
138
+
139
+ # 若版本被 bump,先提交 package.json 改动
140
+ if [[ -n "$BUMP" ]]; then
141
+ git add package.json
142
+ git commit -m "chore: bump version to ${VERSION}"
143
+ ok "已提交版本 bump commit"
144
+ fi
145
+
146
+ if git rev-parse "$TAG" &>/dev/null; then
147
+ err "Tag ${TAG} 已存在,请先删除或检查版本号"
148
+ exit 1
149
+ fi
150
+
151
+ git tag "$TAG"
152
+ ok "已创建 tag ${TAG}"
153
+ )
154
+
155
+ # ────────────────────────────────────────────────────────────────────────────
156
+ # 步骤 5:npm publish(OTP 通过环境变量传入,不出现在命令行参数)
157
+ # ────────────────────────────────────────────────────────────────────────────
158
+ info "准备 npm publish …"
159
+
160
+ if [[ -n "${NPM_OTP:-}" ]]; then
161
+ info "检测到环境变量 NPM_OTP,自动使用"
162
+ OTP="$NPM_OTP"
163
+ else
164
+ read -r -p "$(printf "${CYAN}Authenticator OTP: ${NC}")" OTP
165
+ fi
166
+
167
+ info "执行 npm publish --access public …"
168
+ (cd "$ROOT_DIR" && NPM_CONFIG_OTP="$OTP" npm publish --access public)
169
+ ok "npm publish 完成"
170
+
171
+ # ────────────────────────────────────────────────────────────────────────────
172
+ # 步骤 6:git push + push tags(publish 成功后才推送)
173
+ # ────────────────────────────────────────────────────────────────────────────
174
+ info "推送代码 + tags …"
175
+ (cd "$ROOT_DIR" && git push && git push --tags)
176
+ ok "已推送到远端"
177
+
178
+ # ────────────────────────────────────────────────────────────────────────────
179
+ # 步骤 7:成功
180
+ # ────────────────────────────────────────────────────────────────────────────
181
+ printf "\n"
182
+ ok "🎉 @andyqiu/teamkit@${VERSION} 发布成功!"
183
+ info "安装/升级命令:npm install -g @andyqiu/teamkit"
184
+ info "查看包页面:https://www.npmjs.com/package/@andyqiu/teamkit"