@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -1,298 +1,298 @@
1
- import { createInterface } from "node:readline/promises"
2
- import { stdin as input, stdout as output } from "node:process"
3
- import { ensureGitHubAuth } from "./auth.mjs"
4
- import { listUserRepos, searchRepos, listBranches } from "./api.mjs"
5
- import { ensureRepo, isLocalRepo, listLocalRepos, localBranches, repoLocalPath, removeLocalRepo, syncRepo, hasLocalChanges, getChangedFiles, generateCommitMessage, commitAndPush } from "./workspace.mjs"
6
-
7
- function timeAgo(dateStr) {
8
- if (!dateStr) return ""
9
- const diff = Date.now() - new Date(dateStr).getTime()
10
- const mins = Math.floor(diff / 60000)
11
- if (mins < 1) return "just now"
12
- if (mins < 60) return `${mins}m ago`
13
- const hours = Math.floor(mins / 60)
14
- if (hours < 24) return `${hours}h ago`
15
- const days = Math.floor(hours / 24)
16
- if (days < 30) return `${days}d ago`
17
- return `${Math.floor(days / 30)}mo ago`
18
- }
19
-
20
- function padEnd(str, len) {
21
- return str.length >= len ? str : str + " ".repeat(len - str.length)
22
- }
23
-
24
- function printRepoList(repos, localSet) {
25
- const maxNameLen = Math.min(40, Math.max(...repos.map((r) => r.full_name.length)))
26
- for (let i = 0; i < repos.length; i++) {
27
- const r = repos[i]
28
- const idx = ` \x1b[2m${String(i + 1).padStart(2)}.\x1b[0m `
29
- const name = padEnd(r.full_name, maxNameLen + 2)
30
- const local = localSet.has(r.full_name) ? "\x1b[32m[local]\x1b[0m " : " "
31
- const stars = r.stars > 0 ? `\x1b[33m★${r.stars}\x1b[0m` : ""
32
- const priv = r.private ? " \x1b[31m●\x1b[0m" : ""
33
- const time = `\x1b[2m${timeAgo(r.pushed_at)}\x1b[0m`
34
- console.log(`${idx}${name}${local}${stars}${priv} ${time}`)
35
- }
36
- }
37
-
38
- function printBranchList(branches, localSet, defaultBranch) {
39
- for (let i = 0; i < branches.length; i++) {
40
- const b = branches[i]
41
- const idx = ` \x1b[2m${String(i + 1).padStart(2)}.\x1b[0m `
42
- const isDefault = b.name === defaultBranch ? " \x1b[2m(default)\x1b[0m" : ""
43
- const local = localSet.has(b.name) ? " \x1b[32m[local]\x1b[0m" : ""
44
- const prot = b.protected ? " \x1b[33m🔒\x1b[0m" : ""
45
- console.log(`${idx}${b.name}${isDefault}${local}${prot}`)
46
- }
47
- }
48
-
49
- async function prompt(rl, message) {
50
- const answer = await rl.question(`\x1b[36m > ${message}\x1b[0m`)
51
- return answer.trim()
52
- }
53
-
54
- export async function runGitHubFlow() {
55
- const { token, login } = await ensureGitHubAuth()
56
-
57
- const rl = createInterface({ input, output })
58
-
59
- try {
60
- // --- Repo selection ---
61
- const localRepoList = await listLocalRepos()
62
- const localRepoSet = new Set(localRepoList)
63
-
64
- console.log(`\x1b[2m 👤 @${login}\x1b[0m`)
65
- console.log(`\n\x1b[1m 📦 你的仓库:\x1b[0m \x1b[2m(输入序号选择,或输入关键词搜索)\x1b[0m\n`)
66
-
67
- let repos = await listUserRepos(token)
68
- printRepoList(repos, localRepoSet)
69
-
70
- let selectedRepo = null
71
- while (!selectedRepo) {
72
- const input = await prompt(rl, "")
73
- if (!input) continue
74
-
75
- const num = parseInt(input, 10)
76
- if (!isNaN(num) && num >= 1 && num <= repos.length) {
77
- selectedRepo = repos[num - 1]
78
- } else {
79
- // Search
80
- console.log(`\n\x1b[2m 搜索 "${input}" ...\x1b[0m\n`)
81
- repos = await searchRepos(token, input, login)
82
- if (repos.length === 0) {
83
- console.log(" \x1b[31m未找到匹配的仓库\x1b[0m\n")
84
- repos = await listUserRepos(token)
85
- printRepoList(repos, localRepoSet)
86
- } else {
87
- printRepoList(repos, localRepoSet)
88
- }
89
- }
90
- }
91
-
92
- // --- Branch selection ---
93
- console.log(`\n\x1b[1m 🌿 分支:\x1b[0m \x1b[2m${selectedRepo.full_name}\x1b[0m\n`)
94
-
95
- const branches = await listBranches(token, selectedRepo.owner, selectedRepo.name)
96
- const localPath = repoLocalPath(selectedRepo.full_name)
97
- const existsLocally = await isLocalRepo(selectedRepo.full_name)
98
- const localBranchSet = new Set(existsLocally ? await localBranches(localPath) : [])
99
-
100
- // Sort: default branch first, then local branches, then the rest
101
- branches.sort((a, b) => {
102
- if (a.name === selectedRepo.default_branch) return -1
103
- if (b.name === selectedRepo.default_branch) return 1
104
- const aLocal = localBranchSet.has(a.name) ? 0 : 1
105
- const bLocal = localBranchSet.has(b.name) ? 0 : 1
106
- return aLocal - bLocal
107
- })
108
-
109
- printBranchList(branches, localBranchSet, selectedRepo.default_branch)
110
-
111
- let selectedBranch = null
112
- while (!selectedBranch) {
113
- const input = await prompt(rl, "选择分支: ")
114
- if (!input) continue
115
-
116
- const num = parseInt(input, 10)
117
- if (!isNaN(num) && num >= 1 && num <= branches.length) {
118
- selectedBranch = branches[num - 1].name
119
- } else {
120
- // Direct branch name input
121
- const match = branches.find((b) => b.name === input)
122
- if (match) {
123
- selectedBranch = match.name
124
- } else {
125
- console.log(` \x1b[31m未找到分支 "${input}"\x1b[0m`)
126
- }
127
- }
128
- }
129
-
130
- // --- Local repo action selection ---
131
- let action = "clone"
132
- if (existsLocally) {
133
- console.log(`\n\x1b[33m 📦 本地已存在仓库 ${selectedRepo.full_name}\x1b[0m\n`)
134
- console.log(" 请选择操作:")
135
- console.log(" \x1b[36m1.\x1b[0m 使用本地仓库(不更新)")
136
- console.log(" \x1b[36m2.\x1b[0m 同步云端最新代码(git pull)")
137
- console.log(" \x1b[36m3.\x1b[0m 强制重新克隆(删除本地,重新下载)")
138
- console.log("")
139
-
140
- while (true) {
141
- const choice = await prompt(rl, "选择操作 (1-3): ")
142
- if (choice === "1") {
143
- action = "use-local"
144
- break
145
- } else if (choice === "2") {
146
- action = "sync"
147
- break
148
- } else if (choice === "3") {
149
- action = "reclone"
150
- break
151
- } else {
152
- console.log(" \x1b[31m请输入 1、2 或 3\x1b[0m")
153
- }
154
- }
155
- }
156
-
157
- rl.close()
158
-
159
- // --- Execute action ---
160
- let result
161
- if (action === "use-local") {
162
- console.log(`\n\x1b[36m 📂 使用本地仓库 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
163
- const localPath = repoLocalPath(selectedRepo.full_name)
164
- // Just checkout the branch if needed
165
- const localBranchList = await localBranches(localPath)
166
- if (!localBranchList.includes(selectedBranch)) {
167
- console.log(` \x1b[33m⚠ 本地没有分支 ${selectedBranch},切换到默认分支\x1b[0m`)
168
- selectedBranch = selectedRepo.default_branch
169
- }
170
- result = { path: localPath, isNew: false, action: "use-local" }
171
- } else if (action === "sync") {
172
- console.log(`\n\x1b[36m 📥 同步云端代码 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
173
- result = await syncRepo({
174
- fullName: selectedRepo.full_name,
175
- branch: selectedBranch,
176
- token
177
- })
178
- result.action = "sync"
179
- } else if (action === "reclone") {
180
- console.log(`\n\x1b[36m 🗑️ 删除本地仓库...\x1b[0m`)
181
- await removeLocalRepo(selectedRepo.full_name)
182
- console.log(`\x1b[36m 📥 重新克隆 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
183
- result = await ensureRepo({
184
- fullName: selectedRepo.full_name,
185
- branch: selectedBranch,
186
- token
187
- })
188
- result.action = "reclone"
189
- } else {
190
- // Clone new repo
191
- console.log(`\n\x1b[36m 📥 克隆仓库 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
192
- result = await ensureRepo({
193
- fullName: selectedRepo.full_name,
194
- branch: selectedBranch,
195
- token
196
- })
197
- result.action = "clone"
198
- }
199
-
200
- console.log(` \x1b[2m→ ${result.path}\x1b[0m`)
201
- console.log(`\x1b[32m ✓ 就绪\x1b[0m\n`)
202
-
203
- return { cwd: result.path }
204
- } finally {
205
- rl.close()
206
- }
207
- }
208
-
209
- /**
210
- * REPL 退出后询问用户是否推送代码到 GitHub
211
- * @param {Object} flowResult - runGitHubFlow 返回的结果
212
- */
213
- export async function promptPushChanges(flowResult) {
214
- const { cwd } = flowResult
215
- if (!cwd) return
216
-
217
- // Check if there are any changes
218
- const hasChanges = await hasLocalChanges(cwd)
219
- if (!hasChanges) {
220
- console.log("\n\x1b[2m 没有检测到代码变更\x1b[0m")
221
- return
222
- }
223
-
224
- // Get changed files for display
225
- const changedFiles = await getChangedFiles(cwd)
226
- console.log("\n\x1b[33m 📦 检测到代码变更:\x1b[0m\n")
227
- for (const file of changedFiles.slice(0, 10)) {
228
- console.log(` \x1b[36m${file}\x1b[0m`)
229
- }
230
- if (changedFiles.length > 10) {
231
- console.log(` \x1b[2m... 还有 ${changedFiles.length - 10} 个文件\x1b[0m`)
232
- }
233
- console.log("")
234
-
235
- // Create readline interface
236
- const rl = createInterface({ input, output })
237
-
238
- try {
239
- // Ask user what to do
240
- console.log("\x1b[1m 是否推送到 GitHub?\x1b[0m\n")
241
- console.log(" \x1b[36m1.\x1b[0m 推送变更到云端 (commit & push)")
242
- console.log(" \x1b[36m2.\x1b[0m 放弃变更,保持云端版本")
243
- console.log(" \x1b[36m3.\x1b[0m 稍后手动处理\n")
244
-
245
- let choice = null
246
- while (!choice) {
247
- const answer = await rl.question(`\x1b[36m > 选择 (1-3): \x1b[0m`)
248
- const trimmed = answer.trim()
249
- if (trimmed === "1" || trimmed === "2" || trimmed === "3") {
250
- choice = trimmed
251
- } else {
252
- console.log(" \x1b[31m请输入 1、2 或 3\x1b[0m")
253
- }
254
- }
255
-
256
- if (choice === "1") {
257
- // Get current branch
258
- const { execFile } = await import("node:child_process")
259
- const currentBranch = await new Promise((resolve) => {
260
- execFile("git", ["branch", "--show-current"], { cwd }, (err, stdout) => {
261
- resolve(err ? "main" : stdout.trim())
262
- })
263
- })
264
-
265
- // Get commit message (use default or ask user)
266
- const defaultMessage = await generateCommitMessage(cwd)
267
- const customMessage = await rl.question(`\x1b[36m > 提交信息 [${defaultMessage}]: \x1b[0m`)
268
- const message = customMessage.trim() || defaultMessage
269
-
270
- // Get token
271
- const { getStoredToken: getToken } = await import("./auth.mjs")
272
- const { token } = await getToken() || {}
273
- if (!token) {
274
- console.log("\n \x1b[31m错误: 未找到 GitHub Token,无法推送\x1b[0m")
275
- return
276
- }
277
-
278
- console.log("\n \x1b[36m📤 正在推送...\x1b[0m")
279
- try {
280
- await commitAndPush({ repoPath: cwd, message, branch: currentBranch, token })
281
- console.log(`\x1b[32m ✓ 已成功推送到 GitHub (${currentBranch})\x1b[0m\n`)
282
- } catch (error) {
283
- console.log(`\x1b[31m ✗ 推送失败: ${error.message}\x1b[0m\n`)
284
- }
285
- } else if (choice === "2") {
286
- console.log("\n \x1b[33m⚠ 已放弃本地变更\x1b[0m\n")
287
- // Optionally reset the repo
288
- const { execFile } = await import("node:child_process")
289
- await new Promise((resolve) => {
290
- execFile("git", ["reset", "--hard", "HEAD"], { cwd }, () => resolve())
291
- })
292
- } else {
293
- console.log("\n \x1b[2m已跳过推送,您稍后可以使用 git 命令手动处理\x1b[0m\n")
294
- }
295
- } finally {
296
- rl.close()
297
- }
298
- }
1
+ import { createInterface } from "node:readline/promises"
2
+ import { stdin as input, stdout as output } from "node:process"
3
+ import { ensureGitHubAuth } from "./auth.mjs"
4
+ import { listUserRepos, searchRepos, listBranches } from "./api.mjs"
5
+ import { ensureRepo, isLocalRepo, listLocalRepos, localBranches, repoLocalPath, removeLocalRepo, syncRepo, hasLocalChanges, getChangedFiles, generateCommitMessage, commitAndPush } from "./workspace.mjs"
6
+
7
+ function timeAgo(dateStr) {
8
+ if (!dateStr) return ""
9
+ const diff = Date.now() - new Date(dateStr).getTime()
10
+ const mins = Math.floor(diff / 60000)
11
+ if (mins < 1) return "just now"
12
+ if (mins < 60) return `${mins}m ago`
13
+ const hours = Math.floor(mins / 60)
14
+ if (hours < 24) return `${hours}h ago`
15
+ const days = Math.floor(hours / 24)
16
+ if (days < 30) return `${days}d ago`
17
+ return `${Math.floor(days / 30)}mo ago`
18
+ }
19
+
20
+ function padEnd(str, len) {
21
+ return str.length >= len ? str : str + " ".repeat(len - str.length)
22
+ }
23
+
24
+ function printRepoList(repos, localSet) {
25
+ const maxNameLen = Math.min(40, Math.max(...repos.map((r) => r.full_name.length)))
26
+ for (let i = 0; i < repos.length; i++) {
27
+ const r = repos[i]
28
+ const idx = ` \x1b[2m${String(i + 1).padStart(2)}.\x1b[0m `
29
+ const name = padEnd(r.full_name, maxNameLen + 2)
30
+ const local = localSet.has(r.full_name) ? "\x1b[32m[local]\x1b[0m " : " "
31
+ const stars = r.stars > 0 ? `\x1b[33m★${r.stars}\x1b[0m` : ""
32
+ const priv = r.private ? " \x1b[31m●\x1b[0m" : ""
33
+ const time = `\x1b[2m${timeAgo(r.pushed_at)}\x1b[0m`
34
+ console.log(`${idx}${name}${local}${stars}${priv} ${time}`)
35
+ }
36
+ }
37
+
38
+ function printBranchList(branches, localSet, defaultBranch) {
39
+ for (let i = 0; i < branches.length; i++) {
40
+ const b = branches[i]
41
+ const idx = ` \x1b[2m${String(i + 1).padStart(2)}.\x1b[0m `
42
+ const isDefault = b.name === defaultBranch ? " \x1b[2m(default)\x1b[0m" : ""
43
+ const local = localSet.has(b.name) ? " \x1b[32m[local]\x1b[0m" : ""
44
+ const prot = b.protected ? " \x1b[33m🔒\x1b[0m" : ""
45
+ console.log(`${idx}${b.name}${isDefault}${local}${prot}`)
46
+ }
47
+ }
48
+
49
+ async function prompt(rl, message) {
50
+ const answer = await rl.question(`\x1b[36m > ${message}\x1b[0m`)
51
+ return answer.trim()
52
+ }
53
+
54
+ export async function runGitHubFlow() {
55
+ const { token, login } = await ensureGitHubAuth()
56
+
57
+ const rl = createInterface({ input, output })
58
+
59
+ try {
60
+ // --- Repo selection ---
61
+ const localRepoList = await listLocalRepos()
62
+ const localRepoSet = new Set(localRepoList)
63
+
64
+ console.log(`\x1b[2m 👤 @${login}\x1b[0m`)
65
+ console.log(`\n\x1b[1m 📦 你的仓库:\x1b[0m \x1b[2m(输入序号选择,或输入关键词搜索)\x1b[0m\n`)
66
+
67
+ let repos = await listUserRepos(token)
68
+ printRepoList(repos, localRepoSet)
69
+
70
+ let selectedRepo = null
71
+ while (!selectedRepo) {
72
+ const input = await prompt(rl, "")
73
+ if (!input) continue
74
+
75
+ const num = parseInt(input, 10)
76
+ if (!isNaN(num) && num >= 1 && num <= repos.length) {
77
+ selectedRepo = repos[num - 1]
78
+ } else {
79
+ // Search
80
+ console.log(`\n\x1b[2m 搜索 "${input}" ...\x1b[0m\n`)
81
+ repos = await searchRepos(token, input, login)
82
+ if (repos.length === 0) {
83
+ console.log(" \x1b[31m未找到匹配的仓库\x1b[0m\n")
84
+ repos = await listUserRepos(token)
85
+ printRepoList(repos, localRepoSet)
86
+ } else {
87
+ printRepoList(repos, localRepoSet)
88
+ }
89
+ }
90
+ }
91
+
92
+ // --- Branch selection ---
93
+ console.log(`\n\x1b[1m 🌿 分支:\x1b[0m \x1b[2m${selectedRepo.full_name}\x1b[0m\n`)
94
+
95
+ const branches = await listBranches(token, selectedRepo.owner, selectedRepo.name)
96
+ const localPath = repoLocalPath(selectedRepo.full_name)
97
+ const existsLocally = await isLocalRepo(selectedRepo.full_name)
98
+ const localBranchSet = new Set(existsLocally ? await localBranches(localPath) : [])
99
+
100
+ // Sort: default branch first, then local branches, then the rest
101
+ branches.sort((a, b) => {
102
+ if (a.name === selectedRepo.default_branch) return -1
103
+ if (b.name === selectedRepo.default_branch) return 1
104
+ const aLocal = localBranchSet.has(a.name) ? 0 : 1
105
+ const bLocal = localBranchSet.has(b.name) ? 0 : 1
106
+ return aLocal - bLocal
107
+ })
108
+
109
+ printBranchList(branches, localBranchSet, selectedRepo.default_branch)
110
+
111
+ let selectedBranch = null
112
+ while (!selectedBranch) {
113
+ const input = await prompt(rl, "选择分支: ")
114
+ if (!input) continue
115
+
116
+ const num = parseInt(input, 10)
117
+ if (!isNaN(num) && num >= 1 && num <= branches.length) {
118
+ selectedBranch = branches[num - 1].name
119
+ } else {
120
+ // Direct branch name input
121
+ const match = branches.find((b) => b.name === input)
122
+ if (match) {
123
+ selectedBranch = match.name
124
+ } else {
125
+ console.log(` \x1b[31m未找到分支 "${input}"\x1b[0m`)
126
+ }
127
+ }
128
+ }
129
+
130
+ // --- Local repo action selection ---
131
+ let action = "clone"
132
+ if (existsLocally) {
133
+ console.log(`\n\x1b[33m 📦 本地已存在仓库 ${selectedRepo.full_name}\x1b[0m\n`)
134
+ console.log(" 请选择操作:")
135
+ console.log(" \x1b[36m1.\x1b[0m 使用本地仓库(不更新)")
136
+ console.log(" \x1b[36m2.\x1b[0m 同步云端最新代码(git pull)")
137
+ console.log(" \x1b[36m3.\x1b[0m 强制重新克隆(删除本地,重新下载)")
138
+ console.log("")
139
+
140
+ while (true) {
141
+ const choice = await prompt(rl, "选择操作 (1-3): ")
142
+ if (choice === "1") {
143
+ action = "use-local"
144
+ break
145
+ } else if (choice === "2") {
146
+ action = "sync"
147
+ break
148
+ } else if (choice === "3") {
149
+ action = "reclone"
150
+ break
151
+ } else {
152
+ console.log(" \x1b[31m请输入 1、2 或 3\x1b[0m")
153
+ }
154
+ }
155
+ }
156
+
157
+ rl.close()
158
+
159
+ // --- Execute action ---
160
+ let result
161
+ if (action === "use-local") {
162
+ console.log(`\n\x1b[36m 📂 使用本地仓库 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
163
+ const localPath = repoLocalPath(selectedRepo.full_name)
164
+ // Just checkout the branch if needed
165
+ const localBranchList = await localBranches(localPath)
166
+ if (!localBranchList.includes(selectedBranch)) {
167
+ console.log(` \x1b[33m⚠ 本地没有分支 ${selectedBranch},切换到默认分支\x1b[0m`)
168
+ selectedBranch = selectedRepo.default_branch
169
+ }
170
+ result = { path: localPath, isNew: false, action: "use-local" }
171
+ } else if (action === "sync") {
172
+ console.log(`\n\x1b[36m 📥 同步云端代码 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
173
+ result = await syncRepo({
174
+ fullName: selectedRepo.full_name,
175
+ branch: selectedBranch,
176
+ token
177
+ })
178
+ result.action = "sync"
179
+ } else if (action === "reclone") {
180
+ console.log(`\n\x1b[36m 🗑️ 删除本地仓库...\x1b[0m`)
181
+ await removeLocalRepo(selectedRepo.full_name)
182
+ console.log(`\x1b[36m 📥 重新克隆 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
183
+ result = await ensureRepo({
184
+ fullName: selectedRepo.full_name,
185
+ branch: selectedBranch,
186
+ token
187
+ })
188
+ result.action = "reclone"
189
+ } else {
190
+ // Clone new repo
191
+ console.log(`\n\x1b[36m 📥 克隆仓库 ${selectedRepo.full_name}@${selectedBranch} ...\x1b[0m`)
192
+ result = await ensureRepo({
193
+ fullName: selectedRepo.full_name,
194
+ branch: selectedBranch,
195
+ token
196
+ })
197
+ result.action = "clone"
198
+ }
199
+
200
+ console.log(` \x1b[2m→ ${result.path}\x1b[0m`)
201
+ console.log(`\x1b[32m ✓ 就绪\x1b[0m\n`)
202
+
203
+ return { cwd: result.path }
204
+ } finally {
205
+ rl.close()
206
+ }
207
+ }
208
+
209
+ /**
210
+ * REPL 退出后询问用户是否推送代码到 GitHub
211
+ * @param {Object} flowResult - runGitHubFlow 返回的结果
212
+ */
213
+ export async function promptPushChanges(flowResult) {
214
+ const { cwd } = flowResult
215
+ if (!cwd) return
216
+
217
+ // Check if there are any changes
218
+ const hasChanges = await hasLocalChanges(cwd)
219
+ if (!hasChanges) {
220
+ console.log("\n\x1b[2m 没有检测到代码变更\x1b[0m")
221
+ return
222
+ }
223
+
224
+ // Get changed files for display
225
+ const changedFiles = await getChangedFiles(cwd)
226
+ console.log("\n\x1b[33m 📦 检测到代码变更:\x1b[0m\n")
227
+ for (const file of changedFiles.slice(0, 10)) {
228
+ console.log(` \x1b[36m${file}\x1b[0m`)
229
+ }
230
+ if (changedFiles.length > 10) {
231
+ console.log(` \x1b[2m... 还有 ${changedFiles.length - 10} 个文件\x1b[0m`)
232
+ }
233
+ console.log("")
234
+
235
+ // Create readline interface
236
+ const rl = createInterface({ input, output })
237
+
238
+ try {
239
+ // Ask user what to do
240
+ console.log("\x1b[1m 是否推送到 GitHub?\x1b[0m\n")
241
+ console.log(" \x1b[36m1.\x1b[0m 推送变更到云端 (commit & push)")
242
+ console.log(" \x1b[36m2.\x1b[0m 放弃变更,保持云端版本")
243
+ console.log(" \x1b[36m3.\x1b[0m 稍后手动处理\n")
244
+
245
+ let choice = null
246
+ while (!choice) {
247
+ const answer = await rl.question(`\x1b[36m > 选择 (1-3): \x1b[0m`)
248
+ const trimmed = answer.trim()
249
+ if (trimmed === "1" || trimmed === "2" || trimmed === "3") {
250
+ choice = trimmed
251
+ } else {
252
+ console.log(" \x1b[31m请输入 1、2 或 3\x1b[0m")
253
+ }
254
+ }
255
+
256
+ if (choice === "1") {
257
+ // Get current branch
258
+ const { execFile } = await import("node:child_process")
259
+ const currentBranch = await new Promise((resolve) => {
260
+ execFile("git", ["branch", "--show-current"], { cwd }, (err, stdout) => {
261
+ resolve(err ? "main" : stdout.trim())
262
+ })
263
+ })
264
+
265
+ // Get commit message (use default or ask user)
266
+ const defaultMessage = await generateCommitMessage(cwd)
267
+ const customMessage = await rl.question(`\x1b[36m > 提交信息 [${defaultMessage}]: \x1b[0m`)
268
+ const message = customMessage.trim() || defaultMessage
269
+
270
+ // Get token
271
+ const { getStoredToken: getToken } = await import("./auth.mjs")
272
+ const { token } = await getToken() || {}
273
+ if (!token) {
274
+ console.log("\n \x1b[31m错误: 未找到 GitHub Token,无法推送\x1b[0m")
275
+ return
276
+ }
277
+
278
+ console.log("\n \x1b[36m📤 正在推送...\x1b[0m")
279
+ try {
280
+ await commitAndPush({ repoPath: cwd, message, branch: currentBranch, token })
281
+ console.log(`\x1b[32m ✓ 已成功推送到 GitHub (${currentBranch})\x1b[0m\n`)
282
+ } catch (error) {
283
+ console.log(`\x1b[31m ✗ 推送失败: ${error.message}\x1b[0m\n`)
284
+ }
285
+ } else if (choice === "2") {
286
+ console.log("\n \x1b[33m⚠ 已放弃本地变更\x1b[0m\n")
287
+ // Optionally reset the repo
288
+ const { execFile } = await import("node:child_process")
289
+ await new Promise((resolve) => {
290
+ execFile("git", ["reset", "--hard", "HEAD"], { cwd }, () => resolve())
291
+ })
292
+ } else {
293
+ console.log("\n \x1b[2m已跳过推送,您稍后可以使用 git 命令手动处理\x1b[0m\n")
294
+ }
295
+ } finally {
296
+ rl.close()
297
+ }
298
+ }