@kkelly-offical/kkcode 0.1.7 → 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 +228 -220
- 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 +89 -89
- 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 -2981
- 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 +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- 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 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- 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 +99 -93
- 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/auth.mjs
CHANGED
|
@@ -1,286 +1,294 @@
|
|
|
1
|
-
import { readFile, writeFile, unlink, mkdir } from "node:fs/promises"
|
|
2
|
-
import { dirname } from "node:path"
|
|
3
|
-
import { spawn } from "node:child_process"
|
|
4
|
-
import { githubTokenPath } from "../storage/paths.mjs"
|
|
5
|
-
|
|
6
|
-
const CLIENT_ID = "Ov23liCqhJ6cRaqyv3uA"
|
|
7
|
-
const DEVICE_CODE_URL = "https://github.com/login/device/code"
|
|
8
|
-
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
9
|
-
const SCOPE = "repo"
|
|
10
|
-
|
|
11
|
-
export async function getStoredToken() {
|
|
12
|
-
try {
|
|
13
|
-
const raw = await readFile(githubTokenPath(), "utf8")
|
|
14
|
-
const data = JSON.parse(raw)
|
|
15
|
-
if (data.token && data.login) return data
|
|
16
|
-
return null
|
|
17
|
-
} catch {
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function saveToken(data) {
|
|
23
|
-
const filePath = githubTokenPath()
|
|
24
|
-
await mkdir(dirname(filePath), { recursive: true })
|
|
25
|
-
await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function logout() {
|
|
29
|
-
try {
|
|
30
|
-
await unlink(githubTokenPath())
|
|
31
|
-
return true
|
|
32
|
-
} catch {
|
|
33
|
-
return false
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function validateToken(token) {
|
|
38
|
-
try {
|
|
39
|
-
const res = await fetch("https://api.github.com/user", {
|
|
40
|
-
headers: {
|
|
41
|
-
Authorization: `Bearer ${token}`,
|
|
42
|
-
Accept: "application/vnd.github+json"
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
if (res.ok) {
|
|
46
|
-
const user = await res.json()
|
|
47
|
-
return { valid: true, login: user.login }
|
|
48
|
-
}
|
|
49
|
-
return { valid: false, login: null }
|
|
50
|
-
} catch {
|
|
51
|
-
return { valid: false, login: null }
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function requestDeviceCode() {
|
|
56
|
-
const res = await fetch(DEVICE_CODE_URL, {
|
|
57
|
-
method: "POST",
|
|
58
|
-
headers: {
|
|
59
|
-
Accept: "application/json",
|
|
60
|
-
"Content-Type": "application/json"
|
|
61
|
-
},
|
|
62
|
-
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE })
|
|
63
|
-
})
|
|
64
|
-
if (!res.ok) {
|
|
65
|
-
throw new Error(`GitHub device code request failed: ${res.status}`)
|
|
66
|
-
}
|
|
67
|
-
return res.json()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function sleep(ms) {
|
|
71
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 跨平台打开浏览器
|
|
76
|
-
* @param {string} url - 要打开的 URL
|
|
77
|
-
*/
|
|
78
|
-
function openBrowser(url) {
|
|
79
|
-
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
command = "
|
|
94
|
-
args = [
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
child
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
child
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
continue
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
if (data.error === "
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (
|
|
251
|
-
console.log(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
1
|
+
import { readFile, writeFile, unlink, mkdir } from "node:fs/promises"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { githubTokenPath } from "../storage/paths.mjs"
|
|
5
|
+
|
|
6
|
+
const CLIENT_ID = "Ov23liCqhJ6cRaqyv3uA"
|
|
7
|
+
const DEVICE_CODE_URL = "https://github.com/login/device/code"
|
|
8
|
+
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
9
|
+
const SCOPE = "repo"
|
|
10
|
+
|
|
11
|
+
export async function getStoredToken() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(githubTokenPath(), "utf8")
|
|
14
|
+
const data = JSON.parse(raw)
|
|
15
|
+
if (data.token && data.login) return data
|
|
16
|
+
return null
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function saveToken(data) {
|
|
23
|
+
const filePath = githubTokenPath()
|
|
24
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
25
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function logout() {
|
|
29
|
+
try {
|
|
30
|
+
await unlink(githubTokenPath())
|
|
31
|
+
return true
|
|
32
|
+
} catch {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function validateToken(token) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch("https://api.github.com/user", {
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${token}`,
|
|
42
|
+
Accept: "application/vnd.github+json"
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
if (res.ok) {
|
|
46
|
+
const user = await res.json()
|
|
47
|
+
return { valid: true, login: user.login }
|
|
48
|
+
}
|
|
49
|
+
return { valid: false, login: null }
|
|
50
|
+
} catch {
|
|
51
|
+
return { valid: false, login: null }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function requestDeviceCode() {
|
|
56
|
+
const res = await fetch(DEVICE_CODE_URL, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: "application/json",
|
|
60
|
+
"Content-Type": "application/json"
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({ client_id: CLIENT_ID, scope: SCOPE })
|
|
63
|
+
})
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`GitHub device code request failed: ${res.status}`)
|
|
66
|
+
}
|
|
67
|
+
return res.json()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sleep(ms) {
|
|
71
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 跨平台打开浏览器
|
|
76
|
+
* @param {string} url - 要打开的 URL
|
|
77
|
+
*/
|
|
78
|
+
function openBrowser(url) {
|
|
79
|
+
// Validate URL is a safe https:// URL before passing to shell
|
|
80
|
+
let parsed
|
|
81
|
+
try { parsed = new URL(url) } catch { return false }
|
|
82
|
+
if (parsed.protocol !== "https:") return false
|
|
83
|
+
|
|
84
|
+
const safeUrl = parsed.href
|
|
85
|
+
const platform = process.platform
|
|
86
|
+
let command
|
|
87
|
+
let args
|
|
88
|
+
|
|
89
|
+
if (platform === "win32") {
|
|
90
|
+
command = "cmd"
|
|
91
|
+
args = ["/c", "start", "", safeUrl]
|
|
92
|
+
} else if (platform === "darwin") {
|
|
93
|
+
command = "open"
|
|
94
|
+
args = [safeUrl]
|
|
95
|
+
} else {
|
|
96
|
+
command = "xdg-open"
|
|
97
|
+
args = [safeUrl]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" })
|
|
102
|
+
child.unref()
|
|
103
|
+
return true
|
|
104
|
+
} catch {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 跨平台复制文本到剪贴板
|
|
111
|
+
* @param {string} text - 要复制的文本
|
|
112
|
+
* @returns {boolean} 是否成功
|
|
113
|
+
*/
|
|
114
|
+
function copyToClipboard(text) {
|
|
115
|
+
const platform = process.platform
|
|
116
|
+
let command
|
|
117
|
+
let args
|
|
118
|
+
let input = text
|
|
119
|
+
|
|
120
|
+
if (platform === "win32") {
|
|
121
|
+
// Windows - 使用 clip 命令
|
|
122
|
+
command = "clip"
|
|
123
|
+
args = []
|
|
124
|
+
} else if (platform === "darwin") {
|
|
125
|
+
// macOS - 使用 pbcopy
|
|
126
|
+
command = "pbcopy"
|
|
127
|
+
args = []
|
|
128
|
+
} else {
|
|
129
|
+
// Linux - 尝试 wl-copy (Wayland) 或 xclip (X11)
|
|
130
|
+
try {
|
|
131
|
+
const child = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] })
|
|
132
|
+
child.on("error", () => {}) // prevent unhandled error crash
|
|
133
|
+
child.stdin.write(text)
|
|
134
|
+
child.stdin.end()
|
|
135
|
+
return true
|
|
136
|
+
} catch {
|
|
137
|
+
try {
|
|
138
|
+
const child = spawn("xclip", ["-selection", "clipboard"], { stdio: ["pipe", "ignore", "ignore"] })
|
|
139
|
+
child.on("error", () => {}) // prevent unhandled error crash
|
|
140
|
+
child.stdin.write(text)
|
|
141
|
+
child.stdin.end()
|
|
142
|
+
return true
|
|
143
|
+
} catch {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] })
|
|
151
|
+
child.on("error", () => {}) // prevent unhandled error crash
|
|
152
|
+
child.stdin.write(input)
|
|
153
|
+
child.stdin.end()
|
|
154
|
+
return true
|
|
155
|
+
} catch {
|
|
156
|
+
return false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function pollAccessToken(deviceCode, interval) {
|
|
161
|
+
let retryCount = 0
|
|
162
|
+
const maxRetries = 3
|
|
163
|
+
const deadline = Date.now() + 10 * 60 * 1000 // 10 minute total timeout
|
|
164
|
+
|
|
165
|
+
while (true) {
|
|
166
|
+
if (Date.now() > deadline) {
|
|
167
|
+
throw new Error("Authorization timed out after 10 minutes. Please try again.")
|
|
168
|
+
}
|
|
169
|
+
await sleep(interval * 1000)
|
|
170
|
+
|
|
171
|
+
let res
|
|
172
|
+
try {
|
|
173
|
+
res = await fetch(ACCESS_TOKEN_URL, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
Accept: "application/json",
|
|
177
|
+
"Content-Type": "application/json"
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
client_id: CLIENT_ID,
|
|
181
|
+
device_code: deviceCode,
|
|
182
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
retryCount = 0 // 重置重试计数
|
|
186
|
+
} catch (networkError) {
|
|
187
|
+
retryCount++
|
|
188
|
+
if (retryCount >= maxRetries) {
|
|
189
|
+
throw new Error(`Network error after ${maxRetries} retries: ${networkError.message}`)
|
|
190
|
+
}
|
|
191
|
+
process.stdout.write(`\n \x1b[33m⚠ 网络错误,${maxRetries - retryCount} 秒后重试...\x1b[0m`)
|
|
192
|
+
await sleep(1000)
|
|
193
|
+
process.stdout.write("\r \x1b[K等待授权...")
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let data
|
|
198
|
+
try {
|
|
199
|
+
data = await res.json()
|
|
200
|
+
} catch {
|
|
201
|
+
// 如果无法解析 JSON,可能是网络问题,继续等待
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (data.access_token) {
|
|
206
|
+
return data.access_token
|
|
207
|
+
}
|
|
208
|
+
if (data.error === "authorization_pending") {
|
|
209
|
+
process.stdout.write(".")
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if (data.error === "slow_down") {
|
|
213
|
+
interval = (data.interval || interval) + 1
|
|
214
|
+
process.stdout.write("\n \x1b[33m⚠ 请求过于频繁,已放慢速度...\x1b[0m")
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (data.error === "expired_token") {
|
|
218
|
+
throw new Error("Authorization timed out. Please try again.")
|
|
219
|
+
}
|
|
220
|
+
if (data.error === "access_denied") {
|
|
221
|
+
throw new Error("Authorization denied by user.")
|
|
222
|
+
}
|
|
223
|
+
// 其他错误,记录但继续等待(可能是临时错误)
|
|
224
|
+
process.stdout.write(`\n \x1b[33m⚠ 服务器返回: ${data.error || 'unknown'},继续等待...\x1b[0m`)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function ensureGitHubAuth() {
|
|
229
|
+
// Check stored token
|
|
230
|
+
const stored = await getStoredToken()
|
|
231
|
+
if (stored) {
|
|
232
|
+
const check = await validateToken(stored.token)
|
|
233
|
+
if (check.valid) {
|
|
234
|
+
return { token: stored.token, login: check.login }
|
|
235
|
+
}
|
|
236
|
+
// Token expired, remove it
|
|
237
|
+
await logout()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Start Device Flow
|
|
241
|
+
console.log("\n\x1b[33m🔐 GitHub 账户未登录,正在启动授权...\x1b[0m\n")
|
|
242
|
+
|
|
243
|
+
const deviceData = await requestDeviceCode()
|
|
244
|
+
const { device_code, user_code, verification_uri, interval } = deviceData
|
|
245
|
+
|
|
246
|
+
// 自动复制代码到剪贴板
|
|
247
|
+
const copied = copyToClipboard(user_code)
|
|
248
|
+
|
|
249
|
+
console.log(` 请在浏览器中打开: \x1b[36m\x1b[4m${verification_uri}\x1b[0m`)
|
|
250
|
+
if (copied) {
|
|
251
|
+
console.log(` 输入代码: \x1b[1m\x1b[32m${user_code}\x1b[0m \x1b[2m✅ 已复制到剪贴板\x1b[0m\n`)
|
|
252
|
+
} else {
|
|
253
|
+
console.log(` 输入代码: \x1b[1m\x1b[32m${user_code}\x1b[0m\n`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 自动打开浏览器
|
|
257
|
+
const opened = openBrowser(verification_uri)
|
|
258
|
+
if (opened) {
|
|
259
|
+
console.log(" \x1b[2m已自动打开浏览器,请完成授权...\x1b[0m")
|
|
260
|
+
if (copied) {
|
|
261
|
+
console.log(" \x1b[2m提示: 在 GitHub 页面按 Ctrl+V 粘贴代码\x1b[0m\n")
|
|
262
|
+
} else {
|
|
263
|
+
console.log("")
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
console.log(" \x1b[33m⚠ 无法自动打开浏览器,请手动访问上述链接\x1b[0m\n")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
process.stdout.write(" 等待授权...")
|
|
270
|
+
|
|
271
|
+
const token = await pollAccessToken(device_code, interval || 5)
|
|
272
|
+
|
|
273
|
+
// Get user info
|
|
274
|
+
const userRes = await fetch("https://api.github.com/user", {
|
|
275
|
+
headers: {
|
|
276
|
+
Authorization: `Bearer ${token}`,
|
|
277
|
+
Accept: "application/vnd.github+json"
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
if (!userRes.ok) {
|
|
281
|
+
throw new Error(`Failed to get user info: ${userRes.status}`)
|
|
282
|
+
}
|
|
283
|
+
const user = await userRes.json()
|
|
284
|
+
|
|
285
|
+
await saveToken({
|
|
286
|
+
token,
|
|
287
|
+
login: user.login,
|
|
288
|
+
login_at: new Date().toISOString()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
console.log(` \x1b[32m✓ 已登录为 @${user.login}\x1b[0m\n`)
|
|
292
|
+
|
|
293
|
+
return { token, login: user.login }
|
|
294
|
+
}
|