@kkelly-offical/kkcode 0.1.6 → 0.2.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 (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +19 -2
  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 +90 -0
  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/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2929
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +36 -14
  96. package/src/session/engine.mjs +417 -227
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1081
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -884
  105. package/src/session/loop.mjs +1005 -905
  106. package/src/session/prompt/agent.txt +25 -0
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +28 -6
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +197 -0
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -510
  116. package/src/session/system-prompt.mjs +56 -8
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +17 -4
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -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
+ }