@greatjoy/gy 0.0.0-stage → 0.1.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,3 +1,92 @@
1
- # Temporary Holding Version
1
+ # gy
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 个人脚手架 CLI:在项目内一键发布 cartech-library 组件库。
4
+
5
+ ```
6
+ gy ktcpub
7
+ ```
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ # 公共源全局安装(推荐)
13
+ npm install -g gy
14
+
15
+ # 海尔内网:通过 npm.haier.net 代理同样可安装(nexus 代理公共源)
16
+ npm install -g gy --registry=http://npm.haier.net/
17
+ ```
18
+
19
+ ## 用法
20
+
21
+ ### 一键发布组件库
22
+
23
+ 在 cartech-library 仓库内(任意子目录)执行:
24
+
25
+ ```bash
26
+ gy ktcpub
27
+ ```
28
+
29
+ 向导流程(按需确认,一问一答):
30
+
31
+ 1. **发布环境**:正式版(latest) / Beta 版(beta tag)
32
+ 2. **依赖安装**:haier 源安装(推荐,自动执行 `nrm use haier`) / 跳过
33
+ 3. **组件选择**:空格多选、回车确认(自动扫描全部可发布包并显示当前版本)
34
+ 4. **版本级别**:patch / minor / major(实时预览目标版本,Beta 显示 `-beta.N`)
35
+ 5. **发布内容**:必填,写入 CHANGELOG(随后作为 git commit message)
36
+ 6. **认证**:自动复用 `~/.npmrc` 中 haier 发布源的 token,缺失时才提示输入
37
+ 7. **ver 后暂停**:`changeset version` 更新完版本号与 CHANGELOG 后询问是否继续(此时 git 可见中间变更,可先核对/修改再放行;选择暂停会给出手动继续的命令)
38
+
39
+ 执行流水线(复用仓库既有 changeset 链路,不重新发明):
40
+
41
+ ```
42
+ 正式: [nrm use haier → pnpm i] → 生成 changeset → changeset version → turbo build
43
+ → nrm use haierpub → changeset publish → 回读校验 dist-tag → git add/commit/push
44
+ Beta: [nrm use haier → pnpm i] → 自动 enter beta → 生成 changeset → changeset version
45
+ → 自动 exit beta → turbo build → nrm use haierpub → changeset publish --tag beta
46
+ → 校验 beta tag → git add/commit/push
47
+ ```
48
+
49
+ > 依赖安装与发布前会自动执行 `nrm use haier` / `nrm use haierpub` 切换镜像;若未安装 nrm,会提示你手动执行对应命令切换(按回车确认后继续)。
50
+
51
+ 常用选项:
52
+
53
+ ```bash
54
+ gy ktcpub --dry-run # 只打印将执行的命令,不实际执行
55
+ gy ktcpub --allow-any-branch # 正式发布时跳过 dev 分支校验
56
+ ```
57
+
58
+ ### 其他
59
+
60
+ ```bash
61
+ gy --help # 查看全部子命令
62
+ gy ktcpub --help
63
+ ```
64
+
65
+ ## 开发调试
66
+
67
+ ```bash
68
+ cd "D:\aWork\gy CLI"
69
+ npm install
70
+ npm link # 注册全局 gy 命令(本机调试)
71
+ node bin/gy.js ktcpub --help # 不 link 直接跑
72
+ ```
73
+
74
+ ## 发布 gy 到公共源
75
+
76
+ ```bash
77
+ cd "D:\aWork\gy CLI"
78
+ npm run pub # npm publish --registry=https://registry.npmjs.org/
79
+ ```
80
+
81
+ - 首次发布前需登录公共源:`npm login --registry=https://registry.npmjs.org/`
82
+ - 发布后海尔内网通过 `npm.haier.net` 代理即可安装(nexus proxy 仓库会回源公共 npm)
83
+ - 包名 `gy` 已确认在公共源未被占用
84
+
85
+ ## 配置
86
+
87
+ 常量集中在 `src/config.js`:仓库目录名、haier 安装源/发布源、发布 scope、排除包、正式分支。
88
+
89
+ ## 扩展新命令
90
+
91
+ 1. 在 `src/commands/` 新建命令文件,导出 `xxxCommand(program)` 注册函数
92
+ 2. 在 `src/commands/index.js` 的 `registerCommands` 中引入一行即可
package/bin/gy.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerCommands } from '../src/commands/index.js';
4
+
5
+ const program = new Command();
6
+
7
+ program
8
+ .name('gy')
9
+ .description('gy 个人脚手架 CLI')
10
+ .version('0.1.0');
11
+
12
+ registerCommands(program);
13
+
14
+ program.parseAsync(process.argv).catch((err) => {
15
+ console.error(err?.message || err);
16
+ process.exit(1);
17
+ });
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
- {
1
+ {
2
2
  "name": "@greatjoy/gy",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "个人脚手架 CLI:一键发布 cartech-library 组件库等",
5
+ "type": "module",
6
+ "bin": {
7
+ "gy": "bin/gy.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "test": "node --test",
15
+ "pub": "npm publish --registry=https://registry.npmjs.org/"
16
+ },
17
+ "engines": {
18
+ "node": ">=16"
19
+ },
20
+ "dependencies": {
21
+ "commander": "^13.1.0",
22
+ "inquirer": "^12.4.1"
23
+ },
24
+ "publishConfig": {
25
+ "registry": "https://registry.npmjs.org/",
26
+ "access": "public"
27
+ },
28
+ "license": "ISC"
29
+ }
@@ -0,0 +1,6 @@
1
+ import { ktcpubCommand } from './ktcpub.js';
2
+
3
+ // 命令注册表:新增子命令 = 新增一个命令文件并在此注册
4
+ export function registerCommands(program) {
5
+ ktcpubCommand(program);
6
+ }
@@ -0,0 +1,333 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import inquirer from 'inquirer';
5
+ import { config } from '../config.js';
6
+ import { log } from '../core/log.js';
7
+ import { run, capture, switchNrm } from '../core/run.js';
8
+ import { currentBranch, dirtyFiles } from '../core/git.js';
9
+ import { locateRepo } from '../core/locate.js';
10
+ import { scanPackages, dependents } from '../core/packages.js';
11
+ import { previewTarget } from '../core/version.js';
12
+ import { createChangeset, isPreMode } from '../core/changeset.js';
13
+ import { readAuthToken, writeAuthToken } from '../core/auth.js';
14
+
15
+ // 统一交互主题:问题加粗暖橙(#f60)、高亮橙色反色、选中绿色、辅助信息灰色
16
+ const F60 = '38;2;255;102;0';
17
+ const F60_BG = '48;2;255;102;0';
18
+ const HI_FG = '38;2;17;13;10';
19
+
20
+ const uiTheme = {
21
+ prefix: '',
22
+ spinner: { interval: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] },
23
+ style: {
24
+ message: (text) => `\x1b[1;${F60}m${text}\x1b[0m`,
25
+ answer: (text) => `\x1b[${F60}m${text}\x1b[0m`,
26
+ highlight: (text) => `\x1b[${F60_BG}m\x1b[${HI_FG}m${text}\x1b[0m`,
27
+ selected: (text) => `\x1b[32m${text}\x1b[0m`,
28
+ error: (text) => `\x1b[31m✖ ${text}\x1b[0m`,
29
+ help: (text) => `\x1b[2m${text}\x1b[0m`,
30
+ key: (text) => `\x1b[33m${text}\x1b[0m`,
31
+ muted: (text) => `\x1b[2m${text}\x1b[0m`,
32
+ },
33
+ };
34
+
35
+ const themed = (q) => ({ ...q, theme: uiTheme });
36
+
37
+ // 组件选项:包名暖橙(#f60) + 版本灰色
38
+ const pkgChoice = (p) => ({
39
+ name: `\x1b[${F60}m${p.name}\x1b[0m \x1b[2m(${p.version})\x1b[0m`,
40
+ value: p.name,
41
+ });
42
+
43
+ export function ktcpubCommand(program) {
44
+ program
45
+ .command('ktcpub')
46
+ .description('一键发布 cartech-library 组件(向导式:环境/组件/版本/发布内容)')
47
+ .option('--dry-run', '只打印将执行的命令,不实际执行')
48
+ .option('--allow-any-branch', '正式发布时跳过 dev 分支校验')
49
+ .action((opts) => runKtcpub(opts));
50
+ }
51
+
52
+ async function runKtcpub(opts) {
53
+ const dryRun = !!opts.dryRun;
54
+
55
+ // 1. 定位仓库
56
+ const repoDir = locateRepo(process.cwd());
57
+ if (!repoDir) {
58
+ log.error(`未找到 ${config.repoDirName} 仓库,请在项目目录内执行`);
59
+ process.exit(1);
60
+ }
61
+ log.ok(`定位项目:${repoDir}`);
62
+
63
+ // 2. 预检
64
+ const branch = currentBranch(repoDir);
65
+ log.info(`当前分支:${branch}`);
66
+ const dirty = dirtyFiles(repoDir);
67
+ if (dirty.length) log.warn(`工作区有 ${dirty.length} 处未提交改动(发布后将随版本改动一并提交)`);
68
+
69
+ // 3. 扫描可发布包
70
+ const packages = scanPackages(repoDir);
71
+ if (!packages.length) {
72
+ log.error('未扫描到可发布包');
73
+ process.exit(1);
74
+ }
75
+
76
+ // 4. 交互向导
77
+ const answers = await inquirer.prompt([
78
+ themed({
79
+ type: 'list',
80
+ name: 'env',
81
+ message: '发布环境:',
82
+ choices: [
83
+ { name: '正式版 (stable → latest)', value: 'stable' },
84
+ { name: 'Beta 版 (beta tag)', value: 'beta' },
85
+ ],
86
+ }),
87
+ themed({
88
+ type: 'list',
89
+ name: 'install',
90
+ message: '依赖安装:',
91
+ choices: [
92
+ { name: 'haier 源安装(推荐)', value: 'haier' },
93
+ { name: '跳过(node_modules 已就绪)', value: 'skip' },
94
+ ],
95
+ }),
96
+ {
97
+ type: 'checkbox',
98
+ name: 'selected',
99
+ message: '选择要发布的组件(空格多选,回车确认):',
100
+ pageSize: 20,
101
+ choices: packages.map(pkgChoice),
102
+ theme: { ...uiTheme, icon: { checked: '✔', unchecked: '○', cursor: '' } },
103
+ validate: (v) => (v.length ? true : '至少选择一个组件'),
104
+ },
105
+ themed({
106
+ type: 'rawlist',
107
+ name: 'level',
108
+ message: '版本级别:',
109
+ choices: (answers) =>
110
+ ['patch', 'minor', 'major'].map((level) => {
111
+ const sample = packages.find((p) => p.name === answers.selected[0]);
112
+ const hint = sample
113
+ ? `${sample.name}: ${sample.version} → ${previewTarget(sample.version, level, answers.env === 'beta')}`
114
+ : '';
115
+ return { name: level + (hint ? ` \x1b[2m(${hint})\x1b[0m` : ''), value: level };
116
+ }),
117
+ }),
118
+ themed({
119
+ type: 'input',
120
+ name: 'summary',
121
+ message: '发布内容(必填,写入 CHANGELOG):',
122
+ validate: (v) => (v.trim() ? true : '发布内容不能为空'),
123
+ }),
124
+ ]);
125
+
126
+ const isBeta = answers.env === 'beta';
127
+
128
+ // 5. 依赖联动提示
129
+ const links = dependents(packages, answers.selected);
130
+ if (links.length) {
131
+ log.warn('以下未选中包依赖了所选组件,可能需联动发布:');
132
+ for (const l of links) log.info(` ${l.name}@${l.version} ← 依赖 ${l.dependsOn.join(', ')}`);
133
+ const { addLinked } = await inquirer.prompt([
134
+ themed({
135
+ type: 'confirm',
136
+ name: 'addLinked',
137
+ message: '是否一并发布这些依赖方?',
138
+ default: true,
139
+ }),
140
+ ]);
141
+ if (addLinked) answers.selected.push(...links.map((l) => l.name));
142
+ }
143
+
144
+ // 6. 认证检测(复用 ~/.npmrc,缺失才输入)
145
+ let token = readAuthToken();
146
+ if (!token) {
147
+ const { newToken, saveToken } = await inquirer.prompt([
148
+ themed({
149
+ type: 'password',
150
+ name: 'newToken',
151
+ message: `未检测到 ${config.publishRegistry} 的认证 token,请输入:`,
152
+ }),
153
+ themed({
154
+ type: 'confirm',
155
+ name: 'saveToken',
156
+ message: '是否写入 ~/.npmrc(changeset 发布需要)?',
157
+ default: true,
158
+ }),
159
+ ]);
160
+ if (!newToken.trim()) {
161
+ log.error('未提供 token,已中止');
162
+ process.exit(1);
163
+ }
164
+ token = newToken.trim();
165
+ if (saveToken) writeAuthToken(token);
166
+ }
167
+ log.ok(`发布源认证就绪:${config.publishRegistry}`);
168
+
169
+ // 7. 分支校验(正式发布要求 dev)
170
+ if (!isBeta && branch !== config.stableBranch && !opts.allowAnyBranch) {
171
+ log.error(`正式发布必须在 ${config.stableBranch} 分支(当前:${branch}),可用 --allow-any-branch 跳过`);
172
+ process.exit(1);
173
+ }
174
+
175
+ // 8. 确认清单
176
+ const targets = answers.selected.map((name) => {
177
+ const p = packages.find((x) => x.name === name);
178
+ return ` ${p.name}@${p.version} → ${previewTarget(p.version, answers.level, isBeta)}`;
179
+ });
180
+ log.title('发布确认清单');
181
+ log.info(`环境 :${isBeta ? 'Beta(dist-tag: beta)' : '正式(dist-tag: latest)'}`);
182
+ log.info(`分支 :${branch}`);
183
+ log.info(`安装 :${answers.install === 'haier' ? 'haier 源' : '跳过'}`);
184
+ log.info(`组件 :\n${targets.join('\n')}`);
185
+ log.info(`改动日志:将写入各选中包 CHANGELOG —— ${answers.summary}`);
186
+ log.info(`发布源 :${config.publishRegistry}`);
187
+ const { confirm } = await inquirer.prompt([
188
+ themed({
189
+ type: 'confirm',
190
+ name: 'confirm',
191
+ message: dryRun ? '以上为 dry-run 计划,确认打印命令序列?' : '确认执行发布?',
192
+ default: true,
193
+ }),
194
+ ]);
195
+ if (!confirm) {
196
+ log.info('已取消');
197
+ return;
198
+ }
199
+
200
+ // 9. 执行流水线
201
+ const runOpts = { cwd: repoDir, dryRun };
202
+ let step = 0;
203
+ const next = (title) => log.step(`${++step}. ${title}`);
204
+
205
+ // 9.1 安装依赖(haier 源:先 nrm use haier)
206
+ if (answers.install === 'haier') {
207
+ next('使用 haier 源安装依赖');
208
+ if (!switchNrm(config.nrmInstallSource, runOpts)) {
209
+ log.warn(`未检测到 nrm,请手动执行:nrm use ${config.nrmInstallSource}`);
210
+ if (!dryRun) {
211
+ const { manualSwitchedInstall } = await inquirer.prompt([
212
+ themed({
213
+ type: 'confirm',
214
+ name: 'manualSwitchedInstall',
215
+ message: '已手动切换到 haier 源?',
216
+ default: true,
217
+ }),
218
+ ]);
219
+ if (!manualSwitchedInstall) log.warn('未切换源,按当前全局 registry 继续安装');
220
+ }
221
+ }
222
+ run('pnpm', ['i'], runOpts);
223
+ } else {
224
+ next('跳过依赖安装');
225
+ }
226
+
227
+ // 9.2 beta pre 模式(如需要)与生成 changeset
228
+ if (isBeta && !isPreMode(repoDir)) {
229
+ next('进入 beta pre 模式 (changeset pre enter beta)');
230
+ run('wa', ['changesetDoc', 'enterBeta'], runOpts);
231
+ } else if (isBeta) {
232
+ next('已在 beta pre 模式');
233
+ }
234
+ next('生成 changeset');
235
+ if (!dryRun) {
236
+ const entries = Object.fromEntries(answers.selected.map((name) => [name, answers.level]));
237
+ const changesetFile = createChangeset(repoDir, entries, answers.summary);
238
+ log.info(`已生成 changeset:${path.basename(changesetFile)}(内容将作为各包 CHANGELOG 改动日志)`);
239
+ } else {
240
+ const names = answers.selected.map((n) => `"${n}": ${answers.level}`).join('\n');
241
+ log.info(`[dry-run] 将生成 .changeset/<时间戳>-gy-ktcpub.md:\n---\n${names}\n---\n\n${answers.summary.trim()}`);
242
+ }
243
+
244
+ // 9.3 更新版本与改动日志
245
+ next('更新版本号与改动日志 (changeset version)');
246
+ run('wa', ['changesetDoc', 'ver'], runOpts);
247
+
248
+ // 9.3.1 ver 后暂停:核对版本号与改动日志(此时 git 可见中间变更)
249
+ if (!dryRun) {
250
+ const { proceedAfterVer } = await inquirer.prompt([
251
+ themed({
252
+ type: 'confirm',
253
+ name: 'proceedAfterVer',
254
+ message: '版本号与改动日志已更新(git 可见),是否继续构建/发布?',
255
+ default: true,
256
+ }),
257
+ ]);
258
+ if (!proceedAfterVer) {
259
+ log.warn('已暂停,可查看/修改版本号与 CHANGELOG 后手动执行继续:');
260
+ log.info(' wa changesetDoc tBuild # 构建全部包');
261
+ log.info(' wa changesetDoc pub|pubBeta # 发布正式/Beta');
262
+ if (isBeta) log.info(' (Beta 模式,继续前需先:wa changesetDoc exitBeta)');
263
+ return;
264
+ }
265
+ }
266
+
267
+ // 9.4 Beta:ver 生成 beta 版本号后,先退出 beta pre 模式再继续
268
+ if (isBeta) {
269
+ next('退出 beta pre 模式 (changeset pre exit)');
270
+ run('wa', ['changesetDoc', 'exitBeta'], runOpts);
271
+ }
272
+
273
+ // 9.5 构建
274
+ next('构建全部包 (turbo build)');
275
+ run('wa', ['changesetDoc', 'tBuild'], runOpts);
276
+
277
+ // 9.6 发布(先 nrm use haierpub,认证走 ~/.npmrc 与仓库 .npmrc 已有配置)
278
+ next('发布到 haier 私有源');
279
+ if (!switchNrm(config.nrmPublishSource, runOpts)) {
280
+ log.warn(`未检测到 nrm,请手动执行:nrm use ${config.nrmPublishSource}`);
281
+ if (!dryRun) {
282
+ const { manualSwitchedPublish } = await inquirer.prompt([
283
+ themed({
284
+ type: 'confirm',
285
+ name: 'manualSwitchedPublish',
286
+ message: '已手动切换到 haierpub 源?',
287
+ default: true,
288
+ }),
289
+ ]);
290
+ if (!manualSwitchedPublish) log.warn('未切换源,按当前配置继续发布');
291
+ }
292
+ }
293
+ run('wa', ['changesetDoc', isBeta ? 'pubBeta' : 'pub'], runOpts);
294
+
295
+ // 9.7 回读校验
296
+ next('回读校验 dist-tag');
297
+ if (!dryRun) {
298
+ const tag = isBeta ? 'beta' : 'latest';
299
+ for (const name of answers.selected) {
300
+ try {
301
+ const json = JSON.parse(
302
+ capture('npm', ['view', name, 'dist-tags', '--json', `--registry=${config.publishRegistry}`], { cwd: repoDir }),
303
+ );
304
+ log.ok(`${name} dist-tag ${tag} = ${json[tag] ?? '(未找到)'}`);
305
+ } catch {
306
+ log.warn(`${name} 校验未确认,请人工核验 ${config.publishRegistry}`);
307
+ }
308
+ }
309
+ }
310
+
311
+ // 9.8 git 提交并推送
312
+ next('git 提交并推送');
313
+ if (dryRun) {
314
+ run('git', ['add', '-A'], runOpts);
315
+ run('git', ['commit', '-F', '<临时文件: release: 发布内容>'], runOpts);
316
+ run('git', ['push', 'origin', branch], runOpts);
317
+ } else {
318
+ run('git', ['add', '-A'], { cwd: repoDir });
319
+ const msgFile = path.join(os.tmpdir(), `gy-commit-${Date.now()}.txt`);
320
+ fs.writeFileSync(msgFile, `release: ${answers.summary.trim()}`, 'utf-8');
321
+ run('git', ['commit', '-F', msgFile], { cwd: repoDir });
322
+ fs.rmSync(msgFile, { force: true });
323
+ run('git', ['push', 'origin', branch], { cwd: repoDir });
324
+ log.ok(`已推送 origin/${branch}`);
325
+ }
326
+
327
+ // 10. 汇总
328
+ log.title(isBeta ? 'Beta 发布完成' : '正式发布完成');
329
+ for (const name of answers.selected) log.info(` ${name}`);
330
+ if (isBeta) {
331
+ log.warn('Beta 已自动退出 pre 模式;如需续发下一个 Beta,下次执行会自动重新进入');
332
+ }
333
+ }
package/src/config.js ADDED
@@ -0,0 +1,18 @@
1
+ // gy CLI 全局配置
2
+ export const config = {
3
+ // 定位目标:cartech-library 仓库目录名
4
+ repoDirName: 'cartech-library',
5
+ // haier 安装源(依赖安装)
6
+ installRegistry: 'http://nexus.haier.net/repository/dts-npm-group/',
7
+ // haier 发布源
8
+ publishRegistry: 'https://nexus.haier.net/repository/dts-npm/',
9
+ // 发布包 scope(用于认证检测)
10
+ publishScope: '@catech-front-end-private-npm',
11
+ // 不参与发布的包目录
12
+ excludedPackages: ['dumi-docs'],
13
+ // 正式发布要求的 git 分支
14
+ stableBranch: 'dev',
15
+ // nrm 源名:安装依赖 / 发布前
16
+ nrmInstallSource: 'haier',
17
+ nrmPublishSource: 'haierpub',
18
+ };
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { config } from '../config.js';
5
+
6
+ const AUTH_KEY = '//nexus.haier.net/repository/dts-npm/:_authToken=';
7
+
8
+ // 读取 ~/.npmrc 中发布源已有的 token;无则返回 null
9
+ export function readAuthToken() {
10
+ const npmrc = path.join(os.homedir(), '.npmrc');
11
+ if (!fs.existsSync(npmrc)) return null;
12
+ const content = fs.readFileSync(npmrc, 'utf-8');
13
+ const line = content.split(/\r?\n/).find((l) => l.trim().startsWith(AUTH_KEY));
14
+ return line ? line.trim().slice(AUTH_KEY.length) : null;
15
+ }
16
+
17
+ // 将 token 追加写入 ~/.npmrc(发布认证用)
18
+ export function writeAuthToken(token) {
19
+ const npmrc = path.join(os.homedir(), '.npmrc');
20
+ const line = `${AUTH_KEY}${token}`;
21
+ const existing = fs.existsSync(npmrc) ? fs.readFileSync(npmrc, 'utf-8').replace(/\s+$/, '') : '';
22
+ fs.writeFileSync(npmrc, `${existing}\n${line}\n`, 'utf-8');
23
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // 是否处于 changeset pre(beta)模式
5
+ export function isPreMode(repoDir) {
6
+ return fs.existsSync(path.join(repoDir, '.changeset', 'pre.json'));
7
+ }
8
+
9
+ // 生成 changeset 文件,返回文件路径
10
+ // entries: { "<包名>": "patch" | "minor" | "major" }
11
+ export function createChangeset(repoDir, entries, summary) {
12
+ const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
13
+ const file = path.join(repoDir, '.changeset', `${ts}-gy-ktcpub.md`);
14
+ const frontmatter = Object.entries(entries)
15
+ .map(([name, level]) => `"${name}": ${level}`)
16
+ .join('\n');
17
+ const content = `---\n${frontmatter}\n---\n\n${summary.trim()}\n`;
18
+ fs.writeFileSync(file, content, 'utf-8');
19
+ return file;
20
+ }
@@ -0,0 +1,10 @@
1
+ import { capture } from './run.js';
2
+
3
+ export function currentBranch(cwd) {
4
+ return capture('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
5
+ }
6
+
7
+ export function dirtyFiles(cwd) {
8
+ const out = capture('git', ['status', '--porcelain'], { cwd });
9
+ return out ? out.split('\n').map((l) => l.trim()).filter(Boolean) : [];
10
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+
5
+ // 从 startDir 向上定位 cartech-library 仓库目录;找不到返回 null
6
+ export function locateRepo(startDir) {
7
+ let dir = path.resolve(startDir);
8
+ for (;;) {
9
+ for (const candidate of [path.join(dir, config.repoDirName), path.join(dir, 'packages', config.repoDirName)]) {
10
+ if (
11
+ fs.existsSync(path.join(candidate, 'package.json')) &&
12
+ fs.existsSync(path.join(candidate, '.changeset'))
13
+ ) {
14
+ return candidate;
15
+ }
16
+ }
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir) return null;
19
+ dir = parent;
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ const paint = (code, msg) => `\x1b[${code}m${msg}\x1b[0m`;
2
+
3
+ export const log = {
4
+ step: (msg) => console.log(paint(36, `\n▸ ${msg}`)),
5
+ info: (msg) => console.log(paint(2, msg)),
6
+ ok: (msg) => console.log(paint(32, `✔ ${msg}`)),
7
+ warn: (msg) => console.log(paint(33, `⚠ ${msg}`)),
8
+ error: (msg) => console.error(paint(31, `✖ ${msg}`)),
9
+ title: (msg) => console.log(paint(1, `\n=== ${msg} ===`)),
10
+ };
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { config } from '../config.js';
4
+
5
+ // 扫描 cartech-library/packages 下的可发布包
6
+ // 返回 [{ dir, name, version, dependencies }]
7
+ export function scanPackages(repoDir) {
8
+ const root = path.join(repoDir, 'packages');
9
+ if (!fs.existsSync(root)) return [];
10
+ const result = [];
11
+ for (const dir of fs.readdirSync(root)) {
12
+ if (config.excludedPackages.includes(dir)) continue;
13
+ const pkgPath = path.join(root, dir, 'package.json');
14
+ if (!fs.existsSync(pkgPath)) continue;
15
+ let pkg;
16
+ try {
17
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
18
+ } catch {
19
+ continue;
20
+ }
21
+ if (pkg.private || !pkg.name) continue;
22
+ result.push({
23
+ dir,
24
+ name: pkg.name,
25
+ version: pkg.version || '0.0.0',
26
+ dependencies: { ...(pkg.dependencies || {}), ...(pkg.peerDependencies || {}) },
27
+ });
28
+ }
29
+ return result.sort((a, b) => a.name.localeCompare(b.name));
30
+ }
31
+
32
+ // 找出依赖了选中包的未选中包
33
+ export function dependents(packages, selectedNames) {
34
+ const selected = new Set(selectedNames);
35
+ const list = [];
36
+ for (const pkg of packages) {
37
+ if (selected.has(pkg.name)) continue;
38
+ const hits = Object.keys(pkg.dependencies).filter((d) => selected.has(d));
39
+ if (hits.length) list.push({ name: pkg.name, version: pkg.version, dependsOn: hits });
40
+ }
41
+ return list;
42
+ }
@@ -0,0 +1,53 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+
4
+ // 在目标目录执行命令(Windows 兼容 .cmd,通过 shell 解析 PATH)
5
+ export function run(cmd, args = [], opts = {}) {
6
+ const { cwd = process.cwd(), dryRun = false, env = {} } = opts;
7
+ const display = [cmd, ...args].join(' ');
8
+
9
+ if (dryRun) {
10
+ console.log(` [dry-run] ${display}`);
11
+ return { status: 0 };
12
+ }
13
+
14
+ const mergedEnv = {
15
+ ...process.env,
16
+ // 确保仓库本地 .bin(wa.cmd 等)可被解析
17
+ PATH: `${path.join(cwd, 'node_modules', '.bin')}${path.delimiter}${process.env.PATH || ''}`,
18
+ ...env,
19
+ };
20
+
21
+ const res = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: true, env: mergedEnv });
22
+ if (res.error) throw res.error;
23
+ if (res.status !== 0) throw new Error(`命令执行失败 (exit ${res.status}): ${display}`);
24
+ return res;
25
+ }
26
+
27
+ // 执行并捕获输出(用于版本校验等需要读回结果的场景)
28
+ export function capture(cmd, args = [], opts = {}) {
29
+ const { cwd = process.cwd(), env = {} } = opts;
30
+ const mergedEnv = {
31
+ ...process.env,
32
+ PATH: `${path.join(cwd, 'node_modules', '.bin')}${path.delimiter}${process.env.PATH || ''}`,
33
+ ...env,
34
+ };
35
+ const res = spawnSync(cmd, args, { cwd, encoding: 'utf-8', shell: true, env: mergedEnv });
36
+ if (res.error) throw res.error;
37
+ if (res.status !== 0) throw new Error(`命令执行失败 (exit ${res.status}): ${cmd} ${args.join(' ')}`);
38
+ return res.stdout.trim();
39
+ }
40
+
41
+ // nrm 是否已安装
42
+ export function hasNrm() {
43
+ const res = spawnSync('nrm', ['--version'], { shell: true, stdio: 'pipe' });
44
+ return !res.error && res.status === 0;
45
+ }
46
+
47
+ // 切换 nrm 源;nrm 未安装时返回 false(由调用方提示手动切换)
48
+ export function switchNrm(source, opts = {}) {
49
+ const { cwd = process.cwd(), dryRun = false } = opts;
50
+ if (!hasNrm()) return false;
51
+ run('nrm', ['use', source], { cwd, dryRun });
52
+ return true;
53
+ }
@@ -0,0 +1,27 @@
1
+ // 解析版本号:x.y.z 或 x.y.z-<tag>.N
2
+ export function parseVersion(v) {
3
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z]+)\.(\d+))?$/.exec(String(v).trim());
4
+ if (!m) return null;
5
+ return { major: +m[1], minor: +m[2], patch: +m[3], tag: m[4] || null, tagNum: m[5] ? +m[5] : null };
6
+ }
7
+
8
+ // 按级别计算基础版本增量(patch/minor/major)
9
+ export function bumpBase(v, level) {
10
+ const p = parseVersion(v);
11
+ if (!p) return null;
12
+ if (level === 'patch') return `${p.major}.${p.minor}.${p.patch + 1}`;
13
+ if (level === 'minor') return `${p.major}.${p.minor + 1}.0`;
14
+ if (level === 'major') return `${p.major + 1}.0.0`;
15
+ return null;
16
+ }
17
+
18
+ // 发布目标版本预览:beta 模式递增 pre 编号,正式模式 bump 基础版本
19
+ export function previewTarget(current, level, isBeta) {
20
+ const p = parseVersion(current);
21
+ if (!p) return null;
22
+ if (isBeta) {
23
+ if (p.tag === 'beta') return `${p.major}.${p.minor}.${p.patch}-beta.${(p.tagNum ?? 0) + 1}`;
24
+ return `${bumpBase(current, level)}-beta.0`;
25
+ }
26
+ return bumpBase(current, level);
27
+ }