@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.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/github/flow.mjs
CHANGED
|
@@ -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
|
+
}
|