@kkelly-offical/kkcode 0.1.2
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 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- package/src/util/yaml.mjs +100 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { mkdir, readdir, stat, rm } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { githubReposDir } from "../storage/paths.mjs"
|
|
5
|
+
|
|
6
|
+
function exec(cmd, args, opts = {}) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
execFile(cmd, args, { timeout: 120000, ...opts }, (err, stdout, stderr) => {
|
|
9
|
+
if (err) {
|
|
10
|
+
err.stderr = stderr
|
|
11
|
+
reject(err)
|
|
12
|
+
} else {
|
|
13
|
+
resolve(stdout.trim())
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function repoLocalPath(fullName) {
|
|
20
|
+
const [owner, repo] = fullName.split("/")
|
|
21
|
+
return path.join(githubReposDir(), owner, repo)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function isLocalRepo(fullName) {
|
|
25
|
+
try {
|
|
26
|
+
const p = repoLocalPath(fullName)
|
|
27
|
+
const s = await stat(path.join(p, ".git"))
|
|
28
|
+
return s.isDirectory()
|
|
29
|
+
} catch {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function ensureRepo({ fullName, branch, token }) {
|
|
35
|
+
const localPath = repoLocalPath(fullName)
|
|
36
|
+
const exists = await isLocalRepo(fullName)
|
|
37
|
+
|
|
38
|
+
if (!exists) {
|
|
39
|
+
// Clone
|
|
40
|
+
await mkdir(path.dirname(localPath), { recursive: true })
|
|
41
|
+
const cloneUrl = `https://${token}@github.com/${fullName}.git`
|
|
42
|
+
await exec("git", ["clone", "--depth", "1", "-b", branch, "--single-branch", cloneUrl, localPath])
|
|
43
|
+
// Remove token from remote URL
|
|
44
|
+
const cleanUrl = `https://github.com/${fullName}.git`
|
|
45
|
+
await exec("git", ["remote", "set-url", "origin", cleanUrl], { cwd: localPath })
|
|
46
|
+
// Store token in git credential for this repo
|
|
47
|
+
await configureCredential(localPath, token)
|
|
48
|
+
return { path: localPath, isNew: true }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Existing repo — fetch & checkout
|
|
52
|
+
await configureCredential(localPath, token)
|
|
53
|
+
await exec("git", ["fetch", "origin"], { cwd: localPath })
|
|
54
|
+
|
|
55
|
+
// Check if branch exists locally
|
|
56
|
+
const localBranchList = await localBranches(localPath)
|
|
57
|
+
if (localBranchList.includes(branch)) {
|
|
58
|
+
await exec("git", ["checkout", branch], { cwd: localPath })
|
|
59
|
+
await exec("git", ["pull", "--ff-only", "origin", branch], { cwd: localPath }).catch(() => {
|
|
60
|
+
// pull may fail if diverged, that's ok
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
// Checkout remote branch
|
|
64
|
+
await exec("git", ["checkout", "-b", branch, `origin/${branch}`], { cwd: localPath })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { path: localPath, isNew: false }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function configureCredential(repoPath, token) {
|
|
71
|
+
// Use store credential helper scoped to this repo
|
|
72
|
+
const credentialPath = path.join(repoPath, ".git", "kkcode-credentials")
|
|
73
|
+
const { writeFile } = await import("node:fs/promises")
|
|
74
|
+
await writeFile(credentialPath, `https://x-access-token:${token}@github.com\n`, "utf8")
|
|
75
|
+
await exec("git", ["config", "credential.helper", `store --file="${credentialPath}"`], { cwd: repoPath })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function listLocalRepos() {
|
|
79
|
+
const root = githubReposDir()
|
|
80
|
+
const repos = []
|
|
81
|
+
try {
|
|
82
|
+
const owners = await readdir(root)
|
|
83
|
+
for (const owner of owners) {
|
|
84
|
+
const ownerPath = path.join(root, owner)
|
|
85
|
+
const ownerStat = await stat(ownerPath).catch(() => null)
|
|
86
|
+
if (!ownerStat || !ownerStat.isDirectory()) continue
|
|
87
|
+
const names = await readdir(ownerPath)
|
|
88
|
+
for (const name of names) {
|
|
89
|
+
const gitDir = path.join(ownerPath, name, ".git")
|
|
90
|
+
const gitStat = await stat(gitDir).catch(() => null)
|
|
91
|
+
if (gitStat && gitStat.isDirectory()) {
|
|
92
|
+
repos.push(`${owner}/${name}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// repos dir doesn't exist yet
|
|
98
|
+
}
|
|
99
|
+
return repos
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function localBranches(repoPath) {
|
|
103
|
+
try {
|
|
104
|
+
const out = await exec("git", ["branch", "--list", "--format=%(refname:short)"], { cwd: repoPath })
|
|
105
|
+
return out.split("\n").filter(Boolean)
|
|
106
|
+
} catch {
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function removeLocalRepo(fullName) {
|
|
112
|
+
const localPath = repoLocalPath(fullName)
|
|
113
|
+
try {
|
|
114
|
+
await rm(localPath, { recursive: true, force: true })
|
|
115
|
+
return true
|
|
116
|
+
} catch {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function syncRepo({ fullName, branch, token }) {
|
|
122
|
+
const localPath = repoLocalPath(fullName)
|
|
123
|
+
await configureCredential(localPath, token)
|
|
124
|
+
await exec("git", ["fetch", "origin"], { cwd: localPath })
|
|
125
|
+
|
|
126
|
+
// Check if branch exists locally
|
|
127
|
+
const localBranchList = await localBranches(localPath)
|
|
128
|
+
if (localBranchList.includes(branch)) {
|
|
129
|
+
await exec("git", ["checkout", branch], { cwd: localPath })
|
|
130
|
+
await exec("git", ["pull", "--ff-only", "origin", branch], { cwd: localPath }).catch(() => {
|
|
131
|
+
// pull may fail if diverged, that's ok
|
|
132
|
+
})
|
|
133
|
+
} else {
|
|
134
|
+
// Checkout remote branch
|
|
135
|
+
await exec("git", ["checkout", "-b", branch, `origin/${branch}`], { cwd: localPath })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { path: localPath }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if there are uncommitted changes
|
|
142
|
+
export async function hasLocalChanges(repoPath) {
|
|
143
|
+
try {
|
|
144
|
+
const status = await exec("git", ["status", "--porcelain"], { cwd: repoPath })
|
|
145
|
+
return status.length > 0
|
|
146
|
+
} catch {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get diff stats for display
|
|
152
|
+
export async function getDiffStats(repoPath) {
|
|
153
|
+
try {
|
|
154
|
+
const stats = await exec("git", ["diff", "--stat", "HEAD"], { cwd: repoPath })
|
|
155
|
+
return stats
|
|
156
|
+
} catch {
|
|
157
|
+
return ""
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get changed files list
|
|
162
|
+
export async function getChangedFiles(repoPath) {
|
|
163
|
+
try {
|
|
164
|
+
const output = await exec("git", ["status", "--short"], { cwd: repoPath })
|
|
165
|
+
return output.split("\n").filter(Boolean)
|
|
166
|
+
} catch {
|
|
167
|
+
return []
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Commit and push changes
|
|
172
|
+
export async function commitAndPush({ repoPath, message, branch, token }) {
|
|
173
|
+
await configureCredential(repoPath, token)
|
|
174
|
+
|
|
175
|
+
// Add all changes
|
|
176
|
+
await exec("git", ["add", "-A"], { cwd: repoPath })
|
|
177
|
+
|
|
178
|
+
// Commit
|
|
179
|
+
await exec("git", ["commit", "-m", message], { cwd: repoPath })
|
|
180
|
+
|
|
181
|
+
// Push
|
|
182
|
+
await exec("git", ["push", "origin", branch], { cwd: repoPath })
|
|
183
|
+
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Generate a commit message based on changes
|
|
188
|
+
export async function generateCommitMessage(repoPath) {
|
|
189
|
+
try {
|
|
190
|
+
const files = await getChangedFiles(repoPath)
|
|
191
|
+
if (files.length === 0) return "Update files"
|
|
192
|
+
|
|
193
|
+
// Count types of changes
|
|
194
|
+
const added = files.filter(f => f.startsWith("A") || f.startsWith("??")).length
|
|
195
|
+
const modified = files.filter(f => f.startsWith("M")).length
|
|
196
|
+
const deleted = files.filter(f => f.startsWith("D")).length
|
|
197
|
+
|
|
198
|
+
if (added > 0 && modified === 0 && deleted === 0) {
|
|
199
|
+
return added === 1 ? "Add file" : `Add ${added} files`
|
|
200
|
+
}
|
|
201
|
+
if (modified > 0 && added === 0 && deleted === 0) {
|
|
202
|
+
return modified === 1 ? "Update file" : `Update ${modified} files`
|
|
203
|
+
}
|
|
204
|
+
if (deleted > 0 && added === 0 && modified === 0) {
|
|
205
|
+
return deleted === 1 ? "Remove file" : `Remove ${deleted} files`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `Update files (+${added} ~${modified} -${deleted})`
|
|
209
|
+
} catch {
|
|
210
|
+
return "Update files"
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander"
|
|
3
|
+
import { createThemeCommand } from "./commands/theme.mjs"
|
|
4
|
+
import { createUsageCommand } from "./commands/usage.mjs"
|
|
5
|
+
import { createReviewCommand } from "./commands/review.mjs"
|
|
6
|
+
import { createSessionCommand } from "./commands/session.mjs"
|
|
7
|
+
import { createChatCommand } from "./commands/chat.mjs"
|
|
8
|
+
import { createAgentCommand } from "./commands/agent.mjs"
|
|
9
|
+
import { createMcpCommand } from "./commands/mcp.mjs"
|
|
10
|
+
import { createPermissionCommand } from "./commands/permission.mjs"
|
|
11
|
+
import { createDoctorCommand } from "./commands/doctor.mjs"
|
|
12
|
+
import { createConfigCommand } from "./commands/config.mjs"
|
|
13
|
+
import { createPromptCommand } from "./commands/prompt.mjs"
|
|
14
|
+
import { createLongagentCommand } from "./commands/longagent.mjs"
|
|
15
|
+
import { createHookCommand } from "./commands/hook.mjs"
|
|
16
|
+
import { createCommandCommand } from "./commands/command.mjs"
|
|
17
|
+
import { createRuleCommand } from "./commands/rule.mjs"
|
|
18
|
+
import { createBackgroundCommand } from "./commands/background.mjs"
|
|
19
|
+
import { createInitCommand } from "./commands/init.mjs"
|
|
20
|
+
import { createAuditCommand } from "./commands/audit.mjs"
|
|
21
|
+
import { startRepl } from "./repl.mjs"
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const hasTrust = process.argv.includes("--trust")
|
|
25
|
+
const hasGithub = process.argv.includes("--github")
|
|
26
|
+
|
|
27
|
+
if (hasGithub) {
|
|
28
|
+
const githubArgIndex = process.argv.indexOf("--github")
|
|
29
|
+
const nextArg = process.argv[githubArgIndex + 1]
|
|
30
|
+
|
|
31
|
+
if (nextArg === "logout") {
|
|
32
|
+
const { logout } = await import("./github/auth.mjs")
|
|
33
|
+
const success = await logout()
|
|
34
|
+
if (success) {
|
|
35
|
+
console.log("✓ 已登出 GitHub 账户")
|
|
36
|
+
} else {
|
|
37
|
+
console.log("⚠ 没有已登录的 GitHub 账户")
|
|
38
|
+
}
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { runGitHubFlow, promptPushChanges } = await import("./github/flow.mjs")
|
|
43
|
+
const result = await runGitHubFlow()
|
|
44
|
+
process.chdir(result.cwd)
|
|
45
|
+
await startRepl({ trust: hasTrust })
|
|
46
|
+
// After REPL exits, ask user if they want to push changes
|
|
47
|
+
await promptPushChanges(result)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.argv.length <= 2 || (process.argv.length === 3 && hasTrust)) {
|
|
52
|
+
await startRepl({ trust: hasTrust })
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const program = new Command()
|
|
57
|
+
program.name("kkcode").description("kkcode CLI").version("0.1.2")
|
|
58
|
+
program.addCommand(createChatCommand())
|
|
59
|
+
program.addCommand(createThemeCommand())
|
|
60
|
+
program.addCommand(createUsageCommand())
|
|
61
|
+
program.addCommand(createReviewCommand())
|
|
62
|
+
program.addCommand(createAgentCommand())
|
|
63
|
+
program.addCommand(createMcpCommand())
|
|
64
|
+
program.addCommand(createPermissionCommand())
|
|
65
|
+
program.addCommand(createDoctorCommand())
|
|
66
|
+
program.addCommand(createConfigCommand())
|
|
67
|
+
program.addCommand(createSessionCommand())
|
|
68
|
+
program.addCommand(createPromptCommand())
|
|
69
|
+
program.addCommand(createLongagentCommand())
|
|
70
|
+
program.addCommand(createHookCommand())
|
|
71
|
+
program.addCommand(createCommandCommand())
|
|
72
|
+
program.addCommand(createRuleCommand())
|
|
73
|
+
program.addCommand(createBackgroundCommand())
|
|
74
|
+
program.addCommand(createAuditCommand())
|
|
75
|
+
program.addCommand(createInitCommand())
|
|
76
|
+
await program.parseAsync(process.argv)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main().catch((error) => {
|
|
80
|
+
console.error(error.message)
|
|
81
|
+
process.exit(1)
|
|
82
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
API design conventions:
|
|
2
|
+
- RESTful: resources as nouns, HTTP verbs for actions
|
|
3
|
+
- Consistent response format: { data, error, meta }
|
|
4
|
+
- Input validation at the boundary (controller/handler level)
|
|
5
|
+
- Service layer for business logic, separate from HTTP handling
|
|
6
|
+
- Error responses: appropriate status codes + error message + error code
|
|
7
|
+
- Authentication middleware, not inline checks
|
|
8
|
+
- Pagination: cursor-based or offset-based, consistent across endpoints
|
|
9
|
+
- Versioning: URL prefix (/api/v1/) or header-based
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
C/C++ conventions for this project:
|
|
2
|
+
- C++: prefer modern C++ (C++17/20), RAII for resource management
|
|
3
|
+
- Smart pointers: std::unique_ptr (default), std::shared_ptr (shared ownership)
|
|
4
|
+
- Containers: std::vector, std::unordered_map, std::string
|
|
5
|
+
- Error handling: exceptions for C++, error codes for C, std::expected (C++23)
|
|
6
|
+
- Build: CMake (CMakeLists.txt) or Makefile
|
|
7
|
+
- Headers: #pragma once or include guards, forward declarations to reduce includes
|
|
8
|
+
- Naming: snake_case (Google/STL style) or camelCase (project convention)
|
|
9
|
+
- Testing: Google Test (gtest) or Catch2
|
|
10
|
+
- Memory: avoid raw new/delete, use containers and smart pointers
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Docker conventions for this project:
|
|
2
|
+
- Multi-stage builds: separate build and runtime stages
|
|
3
|
+
- Use specific base image tags, not :latest
|
|
4
|
+
- .dockerignore to exclude node_modules, .git, .env
|
|
5
|
+
- One process per container, use docker-compose for multi-service
|
|
6
|
+
- COPY package*.json first, then npm install (layer caching)
|
|
7
|
+
- Non-root user: USER node or create dedicated user
|
|
8
|
+
- Health checks: HEALTHCHECK CMD curl -f http://localhost:PORT/health
|
|
9
|
+
- Environment variables for configuration, not hardcoded values
|
|
10
|
+
- docker-compose.yml: services, volumes, networks, depends_on
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ASP.NET Core conventions for this project:
|
|
2
|
+
- Minimal API or Controller-based endpoints
|
|
3
|
+
- Dependency injection via builder.Services
|
|
4
|
+
- Entity Framework Core for data access with migrations
|
|
5
|
+
- DTOs separate from domain models
|
|
6
|
+
- Middleware pipeline for cross-cutting concerns
|
|
7
|
+
- appsettings.json for configuration, IOptions<T> pattern
|
|
8
|
+
- xUnit or NUnit for testing, Moq for mocking
|
|
9
|
+
- Async/await throughout, CancellationToken for cancellation
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Electron conventions for this project:
|
|
2
|
+
- Process model: main process (Node.js) + renderer process (Chromium)
|
|
3
|
+
- IPC: ipcMain/ipcRenderer for inter-process communication
|
|
4
|
+
- Preload scripts: contextBridge.exposeInMainWorld() for secure API exposure
|
|
5
|
+
- contextIsolation: true, nodeIntegration: false (security defaults)
|
|
6
|
+
- BrowserWindow for app windows, Menu/Tray for system integration
|
|
7
|
+
- Auto-update: electron-updater for release distribution
|
|
8
|
+
- Packaging: electron-builder or electron-forge
|
|
9
|
+
- File system access through main process, not renderer
|
|
10
|
+
- Store user data in app.getPath('userData')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Flutter/Dart conventions for this project:
|
|
2
|
+
- Widget composition: small, focused widgets over large build() methods
|
|
3
|
+
- StatelessWidget for UI without state, StatefulWidget only when needed
|
|
4
|
+
- State management: Provider, Riverpod, or Bloc pattern
|
|
5
|
+
- Navigation: GoRouter or Navigator 2.0 for declarative routing
|
|
6
|
+
- Naming: snake_case for files, PascalCase for classes, camelCase for variables
|
|
7
|
+
- Async: Future<T> and Stream<T> with async/await
|
|
8
|
+
- Null safety: use required, late, and ? operators properly
|
|
9
|
+
- Testing: widget tests with testWidgets(), unit tests with test()
|
|
10
|
+
- Folder structure: lib/screens/, lib/widgets/, lib/models/, lib/services/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Go conventions for this project:
|
|
2
|
+
- Standard project layout: cmd/, internal/, pkg/
|
|
3
|
+
- Error handling: return errors, don't panic. Wrap with fmt.Errorf("context: %w", err)
|
|
4
|
+
- Interfaces: small, defined by consumer not provider
|
|
5
|
+
- Concurrency: goroutines + channels, or sync.WaitGroup
|
|
6
|
+
- Testing: table-driven tests, testify for assertions
|
|
7
|
+
- Naming: MixedCaps, no underscores. Exported = uppercase first letter
|
|
8
|
+
- Context: pass context.Context as first parameter
|
|
9
|
+
- Dependencies: go mod, minimal external deps
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
GraphQL conventions:
|
|
2
|
+
- Schema-first or code-first design, consistent across project
|
|
3
|
+
- Types: Query for reads, Mutation for writes, Subscription for real-time
|
|
4
|
+
- Naming: camelCase for fields, PascalCase for types, UPPER_CASE for enums
|
|
5
|
+
- Input types for mutation arguments: input CreateUserInput { ... }
|
|
6
|
+
- Pagination: Relay-style connections (edges, nodes, pageInfo) or simple offset
|
|
7
|
+
- Error handling: use errors array with extensions for error codes
|
|
8
|
+
- N+1 prevention: DataLoader for batching and caching
|
|
9
|
+
- Resolvers: thin resolvers that delegate to service layer
|
|
10
|
+
- Authentication: context-based, not per-resolver checks
|