@mindbase/node-tools 1.1.0 → 1.3.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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * npm 适配器
3
+ * 支持发布到 npm 兼容的注册源
4
+ */
5
+
6
+ const { execaCommand } = require('execa');
7
+ const { writeFileSync, unlinkSync, existsSync } = require('fs');
8
+ const { join } = require('path');
9
+ const { BaseAdapter } = require('./base.js');
10
+
11
+ class NpmAdapter extends BaseAdapter {
12
+ /**
13
+ * 发布包到 npm
14
+ */
15
+ async publish(pkg, options) {
16
+ const { tag = 'latest', dryRun = false } = options;
17
+
18
+ // 创建临时 .npmrc,包含 token 认证
19
+ const npmrcPath = join(pkg.path, '.npmrc');
20
+ // npm 官方源使用 //registry.npmjs.org/:_authToken 格式
21
+ const registryUrl = new URL(this.registry);
22
+ const authTokenLine = `${registryUrl.origin}/:_authToken=${this.token}\n`;
23
+ const npmrcContent = `registry=${this.registry}\n${authTokenLine}`;
24
+ writeFileSync(npmrcPath, npmrcContent, 'utf-8');
25
+
26
+ try {
27
+ const args = ['publish'];
28
+ if (tag) args.push('--tag', tag);
29
+ if (dryRun) args.push('--dry-run');
30
+
31
+ await execaCommand('npm publish ' + args.join(' '), {
32
+ cwd: pkg.path,
33
+ stdout: 'inherit',
34
+ stderr: 'inherit'
35
+ });
36
+
37
+ return { success: true };
38
+ } catch (error) {
39
+ return {
40
+ success: false,
41
+ error: error.message || error.stderr || '发布失败'
42
+ };
43
+ } finally {
44
+ // 清理临时 .npmrc
45
+ if (existsSync(npmrcPath)) {
46
+ unlinkSync(npmrcPath);
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 验证 token 有效性
53
+ */
54
+ async validateToken() {
55
+ const { tmpdir } = require('os');
56
+ const { join } = require('path');
57
+ const { writeFileSync, unlinkSync } = require('fs');
58
+
59
+ // 创建临时 .npmrc
60
+ const tmpNpmrcPath = join(tmpdir(), '.npmrc-' + Date.now());
61
+ const registryUrl = new URL(this.registry);
62
+ const authTokenLine = `${registryUrl.origin}/:_authToken=${this.token}\n`;
63
+ const npmrcContent = `registry=${this.registry}\n${authTokenLine}`;
64
+ writeFileSync(tmpNpmrcPath, npmrcContent, 'utf-8');
65
+
66
+ try {
67
+ const result = await execaCommand('npm whoami', {
68
+ env: {
69
+ ...process.env,
70
+ npm_config_userconfig: tmpNpmrcPath
71
+ },
72
+ shell: true
73
+ });
74
+
75
+ // 清理临时文件
76
+ unlinkSync(tmpNpmrcPath);
77
+
78
+ return {
79
+ valid: true,
80
+ username: result.stdout?.trim()
81
+ };
82
+ } catch (error) {
83
+ // 清理临时文件
84
+ try {
85
+ unlinkSync(tmpNpmrcPath);
86
+ } catch (e) {}
87
+
88
+ return {
89
+ valid: false,
90
+ error: error.message || 'Token 验证失败'
91
+ };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 检查包是否存在
97
+ */
98
+ async checkPackage(name, version) {
99
+ try {
100
+ const args = ['view', `${name}@${version}`];
101
+ if (this.registry && this.registry !== 'https://registry.npmjs.org') {
102
+ args.push('--registry', this.registry);
103
+ }
104
+
105
+ await execaCommand('npm ' + args.join(' '), {
106
+ env: {
107
+ ...process.env,
108
+ npm_config_registry: this.registry
109
+ },
110
+ shell: true
111
+ });
112
+
113
+ return { exists: true };
114
+ } catch (error) {
115
+ return { exists: false };
116
+ }
117
+ }
118
+ }
119
+
120
+ module.exports = {
121
+ NpmAdapter
122
+ };
@@ -0,0 +1,275 @@
1
+ /**
2
+ * 全局配置管理模块
3
+ * 统一管理所有发布源和 token
4
+ */
5
+
6
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = require('fs');
7
+ const { join, dirname } = require('path');
8
+ const { homedir } = 'os';
9
+ const os = require('os');
10
+
11
+ // 获取配置文件路径
12
+ function getGlobalConfigPath() {
13
+ const appData = process.env.APPDATA || join(homedir(), '.config');
14
+ return join(appData, 'node-tools', 'publish-config.json');
15
+ }
16
+
17
+ // 默认配置
18
+ const DEFAULT_GLOBAL_CONFIG = {
19
+ registries: [
20
+ {
21
+ id: 'npm-official',
22
+ name: 'npm 官方',
23
+ type: 'npm',
24
+ url: 'https://registry.npmjs.org'
25
+ }
26
+ ],
27
+ tokens: []
28
+ };
29
+
30
+ // 确保配置目录存在
31
+ function ensureConfigDir(configPath) {
32
+ const dir = dirname(configPath);
33
+ if (!existsSync(dir)) {
34
+ mkdirSync(dir, { recursive: true });
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 全局配置管理类
40
+ */
41
+ class GlobalConfig {
42
+ constructor() {
43
+ this.configPath = getGlobalConfigPath();
44
+ }
45
+
46
+ /**
47
+ * 读取全局配置
48
+ */
49
+ getGlobalConfig() {
50
+ if (!existsSync(this.configPath)) {
51
+ // 首次运行,创建默认配置
52
+ ensureConfigDir(this.configPath);
53
+ this.setGlobalConfig(DEFAULT_GLOBAL_CONFIG);
54
+ return JSON.parse(JSON.stringify(DEFAULT_GLOBAL_CONFIG));
55
+ }
56
+
57
+ try {
58
+ const content = readFileSync(this.configPath, 'utf-8');
59
+ return JSON.parse(content);
60
+ } catch (error) {
61
+ console.error('配置文件读取失败,使用默认配置');
62
+ return JSON.parse(JSON.stringify(DEFAULT_GLOBAL_CONFIG));
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 保存全局配置
68
+ */
69
+ setGlobalConfig(config) {
70
+ ensureConfigDir(this.configPath);
71
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
72
+ }
73
+
74
+ /**
75
+ * 获取所有注册源
76
+ */
77
+ getRegistries() {
78
+ const config = this.getGlobalConfig();
79
+ return config.registries || [];
80
+ }
81
+
82
+ /**
83
+ * 根据 ID 获取注册源
84
+ */
85
+ getRegistry(registryId) {
86
+ const registries = this.getRegistries();
87
+ return registries.find(r => r.id === registryId);
88
+ }
89
+
90
+ /**
91
+ * 添加注册源
92
+ */
93
+ addRegistry(id, name, type, url) {
94
+ const config = this.getGlobalConfig();
95
+
96
+ // 检查 ID 是否已存在
97
+ if (config.registries.find(r => r.id === id)) {
98
+ throw new Error(`注册源 ID 已存在: ${id}`);
99
+ }
100
+
101
+ config.registries.push({ id, name, type, url });
102
+ this.setGlobalConfig(config);
103
+
104
+ return { id, name, type, url };
105
+ }
106
+
107
+ /**
108
+ * 删除注册源
109
+ */
110
+ removeRegistry(registryId) {
111
+ const config = this.getGlobalConfig();
112
+
113
+ // 删除关联的 tokens
114
+ config.tokens = config.tokens.filter(t => t.registryId !== registryId);
115
+
116
+ // 删除注册源
117
+ config.registries = config.registries.filter(r => r.id !== registryId);
118
+
119
+ this.setGlobalConfig(config);
120
+ }
121
+
122
+ /**
123
+ * 获取所有 token
124
+ */
125
+ getTokens() {
126
+ const config = this.getGlobalConfig();
127
+ return config.tokens || [];
128
+ }
129
+
130
+ /**
131
+ * 根据 ID 获取 token
132
+ */
133
+ getToken(tokenId) {
134
+ const tokens = this.getTokens();
135
+ return tokens.find(t => t.id === tokenId);
136
+ }
137
+
138
+ /**
139
+ * 获取指定注册源的 tokens
140
+ */
141
+ getTokensByRegistry(registryId) {
142
+ const tokens = this.getTokens();
143
+ return tokens.filter(t => t.registryId === registryId);
144
+ }
145
+
146
+ /**
147
+ * 添加 token
148
+ */
149
+ async addToken(registryId, name, token) {
150
+ const config = this.getGlobalConfig();
151
+
152
+ // 检查注册源是否存在
153
+ const registry = this.getRegistry(registryId);
154
+ if (!registry) {
155
+ throw new Error(`注册源不存在: ${registryId}`);
156
+ }
157
+
158
+ const tokenId = `${registryId}-${Date.now()}`;
159
+ config.tokens.push({
160
+ id: tokenId,
161
+ registryId,
162
+ name,
163
+ token,
164
+ createdAt: new Date().toISOString()
165
+ });
166
+
167
+ this.setGlobalConfig(config);
168
+
169
+ return tokenId;
170
+ }
171
+
172
+ /**
173
+ * 删除 token
174
+ */
175
+ async removeToken(tokenId) {
176
+ const config = this.getGlobalConfig();
177
+ config.tokens = config.tokens.filter(t => t.id !== tokenId);
178
+ this.setGlobalConfig(config);
179
+ }
180
+
181
+ /**
182
+ * 更新 token
183
+ */
184
+ async updateToken(tokenId, newToken) {
185
+ const config = this.getGlobalConfig();
186
+ const tokenData = config.tokens.find(t => t.id === tokenId);
187
+
188
+ if (!tokenData) {
189
+ throw new Error(`Token 不存在: ${tokenId}`);
190
+ }
191
+
192
+ tokenData.token = newToken;
193
+ this.setGlobalConfig(config);
194
+ }
195
+
196
+ /**
197
+ * 显示配置
198
+ */
199
+ showConfig() {
200
+ const config = this.getGlobalConfig();
201
+
202
+ console.log('全局配置:');
203
+ console.log('\n注册源:');
204
+ for (const registry of config.registries) {
205
+ console.log(` ${registry.id} - ${registry.name} (${registry.type})`);
206
+ console.log(` URL: ${registry.url}`);
207
+ }
208
+
209
+ console.log('\nTokens:');
210
+ for (const token of config.tokens) {
211
+ console.log(` ${token.id} - ${token.name}`);
212
+ console.log(` 注册源: ${token.registryId}`);
213
+ console.log(` 创建时间: ${token.createdAt}`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * 编辑配置
219
+ */
220
+ editConfig() {
221
+ const edit = require('editor');
222
+ const { spawn } = require('child_process');
223
+ const { platform } = process;
224
+
225
+ // 确保配置文件存在
226
+ if (!existsSync(this.configPath)) {
227
+ ensureConfigDir(this.configPath);
228
+ this.setGlobalConfig(DEFAULT_GLOBAL_CONFIG);
229
+ }
230
+
231
+ console.log(`打开配置文件: ${this.configPath}`);
232
+
233
+ // 优先使用 code(VS Code)
234
+ const editorCmd = process.env.EDITOR || (platform === 'win32' ? 'code' : 'vim');
235
+
236
+ if (editorCmd === 'code') {
237
+ const spawnOptions = platform === 'win32'
238
+ ? { shell: true, stdio: 'inherit' }
239
+ : { stdio: 'inherit' };
240
+
241
+ spawn('code', [this.configPath, '--wait'], spawnOptions)
242
+ .on('exit', (code) => {
243
+ if (code === 0) {
244
+ console.log('\n配置已保存');
245
+ } else {
246
+ console.log(`\n编辑器退出,代码: ${code}`);
247
+ }
248
+ })
249
+ .on('error', () => {
250
+ console.log('VS Code 不可用,使用默认编辑器...');
251
+ edit(this.configPath, (code) => {
252
+ console.log(code === 0 ? '\n配置已保存' : `\n编辑器退出,代码: ${code}`);
253
+ });
254
+ });
255
+ } else {
256
+ edit(this.configPath, (code) => {
257
+ console.log(code === 0 ? '\n配置已保存' : `\n编辑器退出,代码: ${code}`);
258
+ });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * 重置为默认配置
264
+ */
265
+ resetConfig() {
266
+ this.setGlobalConfig(DEFAULT_GLOBAL_CONFIG);
267
+ return DEFAULT_GLOBAL_CONFIG;
268
+ }
269
+ }
270
+
271
+ module.exports = {
272
+ GlobalConfig,
273
+ getGlobalConfigPath,
274
+ DEFAULT_GLOBAL_CONFIG
275
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * 项目配置管理模块
3
+ * 每个项目记录自己默认的源和 token ID
4
+ */
5
+
6
+ const { existsSync, readFileSync, writeFileSync } = require('fs');
7
+ const { join } = require('path');
8
+
9
+ const PROJECT_CONFIG_FILENAME = '.node-tools-publish.json';
10
+
11
+ const DEFAULT_PROJECT_CONFIG = {
12
+ defaults: {
13
+ registries: [],
14
+ tokens: {} // { registryId: tokenId }
15
+ }
16
+ };
17
+
18
+ /**
19
+ * 项目配置管理类
20
+ */
21
+ class ProjectConfig {
22
+ constructor(projectPath) {
23
+ this.projectPath = projectPath;
24
+ this.configPath = join(projectPath, PROJECT_CONFIG_FILENAME);
25
+ }
26
+
27
+ /**
28
+ * 读取项目配置
29
+ */
30
+ getProjectConfig() {
31
+ if (!existsSync(this.configPath)) {
32
+ return JSON.parse(JSON.stringify(DEFAULT_PROJECT_CONFIG));
33
+ }
34
+
35
+ try {
36
+ const content = readFileSync(this.configPath, 'utf-8');
37
+ return JSON.parse(content);
38
+ } catch (error) {
39
+ console.error('项目配置文件读取失败');
40
+ return JSON.parse(JSON.stringify(DEFAULT_PROJECT_CONFIG));
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 保存项目配置
46
+ */
47
+ setProjectConfig(config) {
48
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
49
+ }
50
+
51
+ /**
52
+ * 获取默认源列表
53
+ */
54
+ getDefaultRegistries() {
55
+ const config = this.getProjectConfig();
56
+ return config.defaults.registries || [];
57
+ }
58
+
59
+ /**
60
+ * 设置默认源列表
61
+ */
62
+ setDefaultRegistries(registryIds) {
63
+ const config = this.getProjectConfig();
64
+ config.defaults.registries = registryIds;
65
+
66
+ // 清理无效的 token 映射
67
+ const newTokens = {};
68
+ for (const registryId of registryIds) {
69
+ if (config.defaults.tokens[registryId]) {
70
+ newTokens[registryId] = config.defaults.tokens[registryId];
71
+ }
72
+ }
73
+ config.defaults.tokens = newTokens;
74
+
75
+ this.setProjectConfig(config);
76
+ }
77
+
78
+ /**
79
+ * 获取指定源的默认 token ID
80
+ */
81
+ getDefaultTokenId(registryId) {
82
+ const config = this.getProjectConfig();
83
+ return config.defaults.tokens[registryId];
84
+ }
85
+
86
+ /**
87
+ * 设置指定源的默认 token ID
88
+ */
89
+ setDefaultToken(registryId, tokenId) {
90
+ const config = this.getProjectConfig();
91
+ config.defaults.tokens[registryId] = tokenId;
92
+ this.setProjectConfig(config);
93
+ }
94
+
95
+ /**
96
+ * 初始化项目配置
97
+ */
98
+ async initConfig(defaultRegistryId, defaultTokenId) {
99
+ const config = this.getProjectConfig();
100
+ config.defaults.registries = [defaultRegistryId];
101
+ config.defaults.tokens = {
102
+ [defaultRegistryId]: defaultTokenId
103
+ };
104
+ this.setProjectConfig(config);
105
+ }
106
+
107
+ /**
108
+ * 显示配置
109
+ */
110
+ showConfig() {
111
+ const config = this.getProjectConfig();
112
+
113
+ console.log('项目配置:');
114
+ console.log(` 配置文件: ${this.configPath}`);
115
+ console.log(`\n默认源: ${config.defaults.registries.join(', ') || '未设置'}`);
116
+
117
+ console.log('\n默认 Tokens:');
118
+ for (const [registryId, tokenId] of Object.entries(config.defaults.tokens)) {
119
+ console.log(` ${registryId}: ${tokenId}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 编辑配置
125
+ */
126
+ editConfig() {
127
+ const edit = require('editor');
128
+ const { spawn } = require('child_process');
129
+ const { platform } = process;
130
+
131
+ // 确保配置文件存在
132
+ if (!existsSync(this.configPath)) {
133
+ this.setProjectConfig(DEFAULT_PROJECT_CONFIG);
134
+ }
135
+
136
+ console.log(`打开项目配置文件: ${this.configPath}`);
137
+
138
+ // 优先使用 code(VS Code)
139
+ const editorCmd = process.env.EDITOR || (platform === 'win32' ? 'code' : 'vim');
140
+
141
+ if (editorCmd === 'code') {
142
+ const spawnOptions = platform === 'win32'
143
+ ? { shell: true, stdio: 'inherit' }
144
+ : { stdio: 'inherit' };
145
+
146
+ spawn('code', [this.configPath, '--wait'], spawnOptions)
147
+ .on('exit', (code) => {
148
+ if (code === 0) {
149
+ console.log('\n配置已保存');
150
+ } else {
151
+ console.log(`\n编辑器退出,代码: ${code}`);
152
+ }
153
+ })
154
+ .on('error', () => {
155
+ console.log('VS Code 不可用,使用默认编辑器...');
156
+ edit(this.configPath, (code) => {
157
+ console.log(code === 0 ? '\n配置已保存' : `\n编辑器退出,代码: ${code}`);
158
+ });
159
+ });
160
+ } else {
161
+ edit(this.configPath, (code) => {
162
+ console.log(code === 0 ? '\n配置已保存' : `\n编辑器退出,代码: ${code}`);
163
+ });
164
+ }
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ ProjectConfig,
170
+ PROJECT_CONFIG_FILENAME,
171
+ DEFAULT_PROJECT_CONFIG
172
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 注册源管理器
3
+ * 协调全局配置、项目配置和适配器
4
+ */
5
+
6
+ const { GlobalConfig } = require('./global-config.js');
7
+ const { ProjectConfig } = require('./project-config.js');
8
+ const { NpmAdapter } = require('./adapters/npm.js');
9
+ const { CodeupAdapter } = require('./adapters/codeup.js');
10
+
11
+ /**
12
+ * 注册源管理器类
13
+ */
14
+ class RegistryManager {
15
+ constructor(globalConfig, projectConfig) {
16
+ this.globalConfig = globalConfig;
17
+ this.projectConfig = projectConfig;
18
+ }
19
+
20
+ /**
21
+ * 创建实例(工厂方法)
22
+ */
23
+ static async create(projectPath) {
24
+ const globalConfig = new GlobalConfig();
25
+ const projectConfig = new ProjectConfig(projectPath);
26
+
27
+ return new RegistryManager(globalConfig, projectConfig);
28
+ }
29
+
30
+ /**
31
+ * 获取项目默认的源和 token
32
+ */
33
+ async getDefaultRegistriesWithTokens() {
34
+ const registryIds = this.projectConfig.getDefaultRegistries();
35
+
36
+ // 如果项目配置为空,返回全局第一个源
37
+ if (registryIds.length === 0) {
38
+ const registries = this.globalConfig.getRegistries();
39
+ if (registries.length > 0) {
40
+ const firstRegistry = registries[0];
41
+ const tokens = this.globalConfig.getTokensByRegistry(firstRegistry.id);
42
+ if (tokens.length > 0) {
43
+ return [{
44
+ registry: firstRegistry,
45
+ tokenId: tokens[0].id,
46
+ token: tokens[0].token
47
+ }];
48
+ }
49
+ }
50
+ return [];
51
+ }
52
+
53
+ const result = [];
54
+
55
+ for (const registryId of registryIds) {
56
+ const registry = this.globalConfig.getRegistry(registryId);
57
+ if (!registry) {
58
+ console.warn(`注册源不存在: ${registryId},跳过`);
59
+ continue;
60
+ }
61
+
62
+ const tokenId = this.projectConfig.getDefaultTokenId(registryId);
63
+ if (!tokenId) {
64
+ console.warn(`注册源 ${registryId} 未配置默认 token,跳过`);
65
+ continue;
66
+ }
67
+
68
+ const tokenData = this.globalConfig.getToken(tokenId);
69
+ if (!tokenData) {
70
+ console.warn(`Token 不存在: ${tokenId},跳过`);
71
+ continue;
72
+ }
73
+
74
+ result.push({
75
+ registry,
76
+ tokenId,
77
+ token: tokenData.token
78
+ });
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * 创建适配器实例
86
+ */
87
+ createAdapter(registry, token) {
88
+ const AdapterClass = {
89
+ 'npm': NpmAdapter,
90
+ 'codeup': CodeupAdapter
91
+ }[registry.type];
92
+
93
+ if (!AdapterClass) {
94
+ throw new Error(`不支持的注册源类型: ${registry.type}`);
95
+ }
96
+
97
+ return new AdapterClass(registry, token);
98
+ }
99
+
100
+ /**
101
+ * 验证 token
102
+ */
103
+ async validateToken(registryId, tokenId) {
104
+ const registry = this.globalConfig.getRegistry(registryId);
105
+ const tokenData = this.globalConfig.getToken(tokenId);
106
+
107
+ if (!registry || !tokenData) {
108
+ return { valid: false, error: '注册源或 token 不存在' };
109
+ }
110
+
111
+ const adapter = this.createAdapter(registry, tokenData.token);
112
+ return await adapter.validateToken();
113
+ }
114
+ }
115
+
116
+ module.exports = {
117
+ RegistryManager
118
+ };