@zjex/git-workflow 0.2.16 → 0.2.18

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/src/ai-service.ts CHANGED
@@ -17,13 +17,6 @@ const AI_PROVIDERS: Record<string, AIProvider> = {
17
17
  free: true,
18
18
  needsKey: true,
19
19
  },
20
- groq: {
21
- name: "Groq",
22
- endpoint: "https://api.groq.com/openai/v1/chat/completions",
23
- defaultModel: "llama-3.1-8b-instant",
24
- free: true,
25
- needsKey: true,
26
- },
27
20
  openai: {
28
21
  name: "OpenAI",
29
22
  endpoint: "https://api.openai.com/v1/chat/completions",
@@ -78,7 +71,8 @@ function buildPrompt(diff: string, language: string): string {
78
71
  2. type 必须是以下之一:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
79
72
  3. scope 是可选的,表示影响范围
80
73
  4. subject 用中文描述,简洁明了,不超过 50 字
81
- 5. 只返回 commit message,不要有其他解释
74
+ 5. 只返回一条 commit message,即使有多个文件改动也要总结成一条
75
+ 6. 不要有其他解释或多余内容
82
76
 
83
77
  示例:
84
78
  - feat(auth): 添加用户登录功能
@@ -91,7 +85,8 @@ Rules:
91
85
  2. type must be one of: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
92
86
  3. scope is optional, indicates the affected area
93
87
  4. subject should be concise, no more than 50 characters
94
- 5. Return only the commit message, no explanations
88
+ 5. Return only ONE commit message, even if multiple files are changed, summarize into one message
89
+ 6. No explanations or extra content
95
90
 
96
91
  Examples:
97
92
  - feat(auth): add user login functionality
@@ -137,38 +132,6 @@ async function callGitHubAPI(
137
132
  return data.choices[0]?.message?.content?.trim() || "";
138
133
  }
139
134
 
140
- /**
141
- * 调用 Groq API
142
- */
143
- async function callGroqAPI(
144
- prompt: string,
145
- apiKey: string,
146
- model: string,
147
- maxTokens: number
148
- ): Promise<string> {
149
- const response = await fetch(AI_PROVIDERS.groq.endpoint, {
150
- method: "POST",
151
- headers: {
152
- Authorization: `Bearer ${apiKey}`,
153
- "Content-Type": "application/json",
154
- },
155
- body: JSON.stringify({
156
- model,
157
- messages: [{ role: "user", content: prompt }],
158
- max_tokens: maxTokens,
159
- temperature: 0.3,
160
- }),
161
- });
162
-
163
- if (!response.ok) {
164
- const error = await response.text();
165
- throw new Error(`Groq API 错误: ${response.status} ${error}`);
166
- }
167
-
168
- const data = await response.json();
169
- return data.choices[0]?.message?.content?.trim() || "";
170
- }
171
-
172
135
  /**
173
136
  * 调用 OpenAI API
174
137
  */
@@ -277,7 +240,7 @@ export async function generateAICommitMessage(
277
240
  config: GwConfig
278
241
  ): Promise<string> {
279
242
  const aiConfig = config.aiCommit || {};
280
- const provider = aiConfig.provider || "groq";
243
+ const provider = aiConfig.provider || "github";
281
244
  const language = aiConfig.language || "zh-CN";
282
245
  const maxTokens = aiConfig.maxTokens || 200;
283
246
 
@@ -316,8 +279,6 @@ export async function generateAICommitMessage(
316
279
  switch (provider) {
317
280
  case "github":
318
281
  return await callGitHubAPI(prompt, apiKey, model, maxTokens);
319
- case "groq":
320
- return await callGroqAPI(prompt, apiKey, model, maxTokens);
321
282
  case "openai":
322
283
  return await callOpenAIAPI(prompt, apiKey, model, maxTokens);
323
284
  case "claude":
@@ -79,22 +79,8 @@ export async function commit(): Promise<void> {
79
79
  const config = getConfig();
80
80
  let { staged, unstaged } = parseGitStatus();
81
81
 
82
- // 没有暂存的更改
83
- if (staged.length === 0) {
84
- if (unstaged.length === 0) {
85
- console.log(colors.yellow("工作区干净,没有需要提交的更改"));
86
- return;
87
- }
88
-
89
- console.log(colors.yellow("没有暂存的更改"));
90
- divider();
91
- console.log("未暂存的文件:");
92
- for (const { status, file } of unstaged) {
93
- console.log(` ${formatFileStatus(status)} ${file}`);
94
- }
95
- divider();
96
-
97
- // 根据配置决定是否自动暂存
82
+ // 如果有未暂存的更改,根据配置决定是否自动暂存
83
+ if (unstaged.length > 0) {
98
84
  const autoStage = config.autoStage ?? true;
99
85
 
100
86
  if (autoStage) {
@@ -105,7 +91,17 @@ export async function commit(): Promise<void> {
105
91
  // 重新获取状态
106
92
  const newStatus = parseGitStatus();
107
93
  staged = newStatus.staged;
108
- } else {
94
+ unstaged = newStatus.unstaged;
95
+ } else if (staged.length === 0) {
96
+ // 没有暂存的文件,且不自动暂存,让用户选择
97
+ console.log(colors.yellow("没有暂存的更改"));
98
+ divider();
99
+ console.log("未暂存的文件:");
100
+ for (const { status, file } of unstaged) {
101
+ console.log(` ${formatFileStatus(status)} ${file}`);
102
+ }
103
+ divider();
104
+
109
105
  // 让用户选择要暂存的文件
110
106
  const filesToStage = await checkbox({
111
107
  message: "选择要暂存的文件:",
@@ -133,14 +129,21 @@ export async function commit(): Promise<void> {
133
129
  const newStatus = parseGitStatus();
134
130
  staged = newStatus.staged;
135
131
  }
136
- } else {
137
- console.log("已暂存的文件:");
138
- for (const { status, file } of staged) {
139
- console.log(` ${formatFileStatus(status)} ${file}`);
140
- }
141
- divider();
142
132
  }
143
133
 
134
+ // 没有暂存的更改
135
+ if (staged.length === 0) {
136
+ console.log(colors.yellow("工作区干净,没有需要提交的更改"));
137
+ return;
138
+ }
139
+
140
+ // 显示已暂存的文件
141
+ console.log("已暂存的文件:");
142
+ for (const { status, file } of staged) {
143
+ console.log(` ${formatFileStatus(status)} ${file}`);
144
+ }
145
+ divider();
146
+
144
147
  // 询问用户选择手动还是 AI 生成
145
148
  const aiAvailable = isAICommitAvailable(config);
146
149
  let commitMode: "ai" | "manual" = "manual";
@@ -33,7 +33,10 @@ Tag 命令:
33
33
  gw r 同上 (别名)
34
34
 
35
35
  配置命令:
36
- gw init 初始化配置文件 .gwrc.json
36
+ gw init 初始化配置文件
37
+ • 全局配置: ~/.gwrc.json (所有项目生效)
38
+ • 项目配置: .gwrc.json (仅当前项目)
39
+ 运行时可选择配置范围
37
40
 
38
41
  Stash 命令:
39
42
  gw stash 交互式管理 stash
@@ -1,10 +1,10 @@
1
1
  import { existsSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
2
4
  import { select, input } from "@inquirer/prompts";
3
5
  import { colors, theme, divider } from "../utils.js";
4
6
  import type { GwConfig } from "../config.js";
5
7
 
6
- const CONFIG_FILE = ".gwrc.json";
7
-
8
8
  // 默认的 commit emoji 配置
9
9
  const DEFAULT_COMMIT_EMOJIS = {
10
10
  feat: "✨",
@@ -21,9 +21,34 @@ const DEFAULT_COMMIT_EMOJIS = {
21
21
  };
22
22
 
23
23
  export async function init(): Promise<void> {
24
- if (existsSync(CONFIG_FILE)) {
24
+ console.log("");
25
+ console.log(colors.bold("⚙️ 初始化 git-workflow 配置"));
26
+ console.log("");
27
+
28
+ // 选择配置范围
29
+ const configScope = await select({
30
+ message: "选择配置范围:",
31
+ choices: [
32
+ {
33
+ name: "全局配置(所有项目生效)",
34
+ value: "global",
35
+ description: "保存到 ~/.gwrc.json,所有项目都会使用此配置",
36
+ },
37
+ {
38
+ name: "项目配置(仅当前项目)",
39
+ value: "project",
40
+ description: "保存到当前目录 .gwrc.json,仅当前项目使用",
41
+ },
42
+ ],
43
+ theme,
44
+ });
45
+
46
+ const isGlobal = configScope === "global";
47
+ const configFile = isGlobal ? join(homedir(), ".gwrc.json") : ".gwrc.json";
48
+
49
+ if (existsSync(configFile)) {
25
50
  const overwrite = await select({
26
- message: `${CONFIG_FILE} 已存在,是否覆盖?`,
51
+ message: `${isGlobal ? "全局" : "项目"}配置文件已存在,是否覆盖?`,
27
52
  choices: [
28
53
  { name: "否,取消", value: false },
29
54
  { name: "是,覆盖", value: true },
@@ -55,14 +80,14 @@ export async function init(): Promise<void> {
55
80
  default: "feature",
56
81
  theme,
57
82
  });
58
- if (featurePrefix !== "feature") config.featurePrefix = featurePrefix;
83
+ config.featurePrefix = featurePrefix;
59
84
 
60
85
  const hotfixPrefix = await input({
61
86
  message: "Hotfix 分支前缀:",
62
87
  default: "hotfix",
63
88
  theme,
64
89
  });
65
- if (hotfixPrefix !== "hotfix") config.hotfixPrefix = hotfixPrefix;
90
+ config.hotfixPrefix = hotfixPrefix;
66
91
 
67
92
  divider();
68
93
 
@@ -75,21 +100,21 @@ export async function init(): Promise<void> {
75
100
  ],
76
101
  theme,
77
102
  });
78
- if (requireId) config.requireId = true;
103
+ config.requireId = requireId;
79
104
 
80
105
  const featureIdLabel = await input({
81
106
  message: "Feature 分支 ID 标签:",
82
107
  default: "Story ID",
83
108
  theme,
84
109
  });
85
- if (featureIdLabel !== "Story ID") config.featureIdLabel = featureIdLabel;
110
+ config.featureIdLabel = featureIdLabel;
86
111
 
87
112
  const hotfixIdLabel = await input({
88
113
  message: "Hotfix 分支 ID 标签:",
89
114
  default: "Issue ID",
90
115
  theme,
91
116
  });
92
- if (hotfixIdLabel !== "Issue ID") config.hotfixIdLabel = hotfixIdLabel;
117
+ config.hotfixIdLabel = hotfixIdLabel;
93
118
 
94
119
  divider();
95
120
 
@@ -110,8 +135,12 @@ export async function init(): Promise<void> {
110
135
  ],
111
136
  theme,
112
137
  });
113
- if (autoPushChoice === "yes") config.autoPush = true;
114
- if (autoPushChoice === "no") config.autoPush = false;
138
+ if (autoPushChoice === "yes") {
139
+ config.autoPush = true;
140
+ } else if (autoPushChoice === "no") {
141
+ config.autoPush = false;
142
+ }
143
+ // autoPushChoice === "ask" 时不设置,使用默认行为(每次询问)
115
144
 
116
145
  divider();
117
146
 
@@ -124,7 +153,7 @@ export async function init(): Promise<void> {
124
153
  ],
125
154
  theme,
126
155
  });
127
- if (!autoStage) config.autoStage = false;
156
+ config.autoStage = autoStage;
128
157
 
129
158
  const useEmoji = await select({
130
159
  message: "Commit 时是否使用 emoji?",
@@ -134,7 +163,7 @@ export async function init(): Promise<void> {
134
163
  ],
135
164
  theme,
136
165
  });
137
- if (!useEmoji) config.useEmoji = false;
166
+ config.useEmoji = useEmoji;
138
167
 
139
168
  // 始终写入默认的 commitEmojis 配置,方便用户修改
140
169
  config.commitEmojis = DEFAULT_COMMIT_EMOJIS;
@@ -164,11 +193,6 @@ export async function init(): Promise<void> {
164
193
  value: "github",
165
194
  description: "使用 GitHub 账号,每天 150 次免费",
166
195
  },
167
- {
168
- name: "Groq(免费)",
169
- value: "groq",
170
- description: "需要注册,每天 14,400 次免费",
171
- },
172
196
  {
173
197
  name: "OpenAI(付费)",
174
198
  value: "openai",
@@ -188,28 +212,42 @@ export async function init(): Promise<void> {
188
212
  theme,
189
213
  });
190
214
 
191
- const useBuiltinKey = await select({
192
- message: "API Key 配置:",
193
- choices: [
194
- {
195
- name: "使用内置 Key(开箱即用)",
196
- value: true,
197
- description: "使用工具内置的 API key,共享限额",
198
- },
199
- {
200
- name: "使用自己的 Key(推荐)",
201
- value: false,
202
- description: "配置自己的 API key,独享限额",
215
+ let apiKey = "";
216
+
217
+ // GitHub Models 需要配置 GitHub Token
218
+ if (aiProvider === "github") {
219
+ console.log("");
220
+ console.log(colors.cyan("💡 如何获取 GitHub Token:"));
221
+ console.log(
222
+ colors.dim(" 1. 访问: https://github.com/settings/tokens/new")
223
+ );
224
+ console.log(colors.dim(" 2. 勾选 'repo' 权限"));
225
+ console.log(colors.dim(" 3. 生成并复制 token"));
226
+ console.log("");
227
+
228
+ apiKey = await input({
229
+ message: "输入你的 GitHub Token:",
230
+ validate: (value) => {
231
+ if (!value.trim()) return "GitHub Token 不能为空";
232
+ return true;
203
233
  },
204
- ],
205
- theme,
206
- });
234
+ theme,
235
+ });
236
+ } else if (aiProvider !== "ollama") {
237
+ // OpenAI 和 Claude 必须配置 API key
238
+ console.log("");
239
+ if (aiProvider === "openai") {
240
+ console.log(colors.cyan("💡 如何获取 OpenAI API Key:"));
241
+ console.log(colors.dim(" 访问: https://platform.openai.com/api-keys"));
242
+ } else {
243
+ console.log(colors.cyan("💡 如何获取 Claude API Key:"));
244
+ console.log(colors.dim(" 访问: https://console.anthropic.com/"));
245
+ }
246
+ console.log("");
207
247
 
208
- let apiKey = "";
209
- if (!useBuiltinKey) {
210
248
  apiKey = await input({
211
249
  message: `输入你的 ${
212
- aiProvider === "github" ? "GitHub Token" : "API Key"
250
+ aiProvider === "openai" ? "OpenAI API Key" : "Claude API Key"
213
251
  }:`,
214
252
  validate: (value) => {
215
253
  if (!value.trim()) return "API Key 不能为空";
@@ -230,12 +268,7 @@ export async function init(): Promise<void> {
230
268
 
231
269
  config.aiCommit = {
232
270
  enabled: true,
233
- provider: aiProvider as
234
- | "github"
235
- | "groq"
236
- | "openai"
237
- | "claude"
238
- | "ollama",
271
+ provider: aiProvider as "github" | "openai" | "claude" | "ollama",
239
272
  apiKey: apiKey || undefined,
240
273
  language: language as "zh-CN" | "en-US",
241
274
  };
@@ -243,7 +276,6 @@ export async function init(): Promise<void> {
243
276
  // 根据提供商设置默认模型
244
277
  const defaultModels: Record<string, string> = {
245
278
  github: "gpt-4o-mini",
246
- groq: "llama-3.1-8b-instant",
247
279
  openai: "gpt-4o-mini",
248
280
  claude: "claude-3-haiku-20240307",
249
281
  ollama: "qwen2.5-coder:7b",
@@ -259,15 +291,35 @@ export async function init(): Promise<void> {
259
291
 
260
292
  // 写入配置
261
293
  const content = JSON.stringify(config, null, 2);
262
- writeFileSync(CONFIG_FILE, content + "\n");
294
+ writeFileSync(configFile, content + "\n");
263
295
 
264
- console.log(colors.green(`✓ 配置已保存到 ${CONFIG_FILE}`));
265
296
  console.log(
266
- colors.dim(
267
- "\n提示: 可以在配置文件中修改 commitEmojis 来自定义各类型的 emoji"
297
+ colors.green(
298
+ `✓ 配置已保存到 ${
299
+ isGlobal ? "全局配置文件" : "项目配置文件"
300
+ }: ${configFile}`
268
301
  )
269
302
  );
270
303
 
304
+ if (isGlobal) {
305
+ console.log("");
306
+ console.log(colors.cyan("💡 提示:"));
307
+ console.log(
308
+ colors.dim(" • 全局配置对所有项目生效,无需在每个项目中重复配置")
309
+ );
310
+ console.log(
311
+ colors.dim(" • 如需为特定项目自定义配置,可在项目中运行 gw init")
312
+ );
313
+ console.log(colors.dim(" • 项目配置会覆盖全局配置"));
314
+ } else {
315
+ console.log("");
316
+ console.log(
317
+ colors.dim(
318
+ "提示: 可以在配置文件中修改 commitEmojis 来自定义各类型的 emoji"
319
+ )
320
+ );
321
+ }
322
+
271
323
  if (config.aiCommit?.enabled) {
272
324
  console.log(
273
325
  colors.dim(
package/src/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
+ import { homedir } from "os";
3
4
  import { execOutput } from "./utils.js";
4
5
 
5
6
  export interface GwConfig {
@@ -39,7 +40,7 @@ export interface GwConfig {
39
40
  // AI commit 配置
40
41
  aiCommit?: {
41
42
  enabled?: boolean; // 是否启用 AI commit,默认 true
42
- provider?: "github" | "groq" | "openai" | "claude" | "ollama"; // AI 提供商,默认 github
43
+ provider?: "github" | "openai" | "claude" | "ollama"; // AI 提供商,默认 github
43
44
  apiKey?: string; // API key,空则使用内置 key
44
45
  model?: string; // 模型名称
45
46
  language?: "zh-CN" | "en-US"; // 生成语言,默认 zh-CN
@@ -89,21 +90,55 @@ function findConfigFile(): string | null {
89
90
  return null;
90
91
  }
91
92
 
93
+ function findGlobalConfigFile(): string | null {
94
+ const globalConfigPath = join(homedir(), ".gwrc.json");
95
+ return existsSync(globalConfigPath) ? globalConfigPath : null;
96
+ }
97
+
92
98
  export function loadConfig(): GwConfig {
93
- const configPath = findConfigFile();
99
+ let config = { ...defaultConfig };
94
100
 
95
- if (!configPath) {
96
- return defaultConfig;
101
+ // 1. 先加载全局配置
102
+ const globalConfigPath = findGlobalConfigFile();
103
+ if (globalConfigPath) {
104
+ try {
105
+ const content = readFileSync(globalConfigPath, "utf-8");
106
+ const globalConfig = JSON.parse(content) as Partial<GwConfig>;
107
+ // 深度合并 aiCommit 配置
108
+ config = {
109
+ ...config,
110
+ ...globalConfig,
111
+ aiCommit: {
112
+ ...config.aiCommit,
113
+ ...globalConfig.aiCommit,
114
+ },
115
+ };
116
+ } catch (e) {
117
+ console.warn(`全局配置文件解析失败: ${globalConfigPath}`);
118
+ }
97
119
  }
98
120
 
99
- try {
100
- const content = readFileSync(configPath, "utf-8");
101
- const userConfig = JSON.parse(content) as Partial<GwConfig>;
102
- return { ...defaultConfig, ...userConfig };
103
- } catch (e) {
104
- console.warn(`配置文件解析失败: ${configPath}`);
105
- return defaultConfig;
121
+ // 2. 再加载项目配置(会覆盖全局配置)
122
+ const projectConfigPath = findConfigFile();
123
+ if (projectConfigPath) {
124
+ try {
125
+ const content = readFileSync(projectConfigPath, "utf-8");
126
+ const projectConfig = JSON.parse(content) as Partial<GwConfig>;
127
+ // 深度合并 aiCommit 配置
128
+ config = {
129
+ ...config,
130
+ ...projectConfig,
131
+ aiCommit: {
132
+ ...config.aiCommit,
133
+ ...projectConfig.aiCommit,
134
+ },
135
+ };
136
+ } catch (e) {
137
+ console.warn(`项目配置文件解析失败: ${projectConfigPath}`);
138
+ }
106
139
  }
140
+
141
+ return config;
107
142
  }
108
143
 
109
144
  // 全局配置实例
package/src/index.ts CHANGED
@@ -66,7 +66,7 @@ async function mainMenu(): Promise<void> {
66
66
  ╚══════╝ ╚════╝ ╚══════╝╚═╝ ╚═╝
67
67
  `)
68
68
  );
69
- console.log(colors.dim(` git-workflow v${version}\n`));
69
+ console.log(colors.dim(` git-workflow v${colors.yellow(version)}\n`));
70
70
 
71
71
  const action = await select({
72
72
  message: "选择操作:",
@@ -287,6 +287,15 @@ cli.help((sections) => {
287
287
  body: showHelp(),
288
288
  });
289
289
  });
290
- cli.version(version);
290
+
291
+ // 不使用 cac 的 version,手动处理 --version
292
+ cli.option("-v, --version", "显示版本号");
293
+
294
+ // 在 parse 之前检查 --version
295
+ const args = process.argv.slice(2);
296
+ if (args.includes("-v") || args.includes("--version")) {
297
+ console.log(colors.yellow(`v${version}`));
298
+ process.exit(0);
299
+ }
291
300
 
292
301
  cli.parse();
@@ -145,19 +145,8 @@ async function performUpdate(packageName: string): Promise<void> {
145
145
  }).start();
146
146
 
147
147
  try {
148
- // 先卸载当前版本,确保干净安装
149
- try {
150
- execSync(`npm uninstall -g ${packageName}`, {
151
- encoding: "utf-8",
152
- stdio: ["pipe", "pipe", "pipe"],
153
- });
154
- spinner.text = "正在安装新版本...";
155
- } catch {
156
- // 当前版本不存在,忽略错误
157
- }
158
-
159
- // 执行安装命令
160
- execSync(`npm install -g ${packageName}`, {
148
+ // 直接安装最新版本(npm 会自动覆盖旧版本)
149
+ execSync(`npm install -g ${packageName}@latest`, {
161
150
  encoding: "utf-8",
162
151
  stdio: ["pipe", "pipe", "pipe"],
163
152
  });
@@ -169,11 +158,7 @@ async function performUpdate(packageName: string): Promise<void> {
169
158
  [
170
159
  colors.bold("✨ 更新完成!"),
171
160
  "",
172
- colors.dim("请运行以下命令刷新并使用新版本:"),
173
- "",
174
- colors.yellow(" hash -r && gw --version"),
175
- "",
176
- colors.dim("或者重新打开终端"),
161
+ colors.dim("请重新打开终端使用新版本"),
177
162
  ].join("\n"),
178
163
  {
179
164
  padding: 1,
@@ -191,7 +176,7 @@ async function performUpdate(packageName: string): Promise<void> {
191
176
  spinner.fail(colors.red("更新失败"));
192
177
  console.log("");
193
178
  console.log(colors.dim(" 你可以手动运行以下命令更新:"));
194
- console.log(colors.cyan(` npm install -g ${packageName}`));
179
+ console.log(colors.cyan(` npm install -g ${packageName}@latest`));
195
180
  console.log("");
196
181
  }
197
182
  }