@icode-js/icode 3.0.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/README.md +346 -0
- package/bin/icode.js +6 -0
- package/package.json +34 -0
- package/src/cli.js +131 -0
- package/src/commands/ai.js +287 -0
- package/src/commands/checkout.js +59 -0
- package/src/commands/clean.js +65 -0
- package/src/commands/codereview.js +52 -0
- package/src/commands/config.js +513 -0
- package/src/commands/explain.js +80 -0
- package/src/commands/help.js +49 -0
- package/src/commands/info.js +57 -0
- package/src/commands/migrate.js +86 -0
- package/src/commands/push.js +125 -0
- package/src/commands/sync.js +74 -0
- package/src/commands/tag.js +53 -0
- package/src/commands/undo.js +66 -0
- package/src/core/ai-client.js +1125 -0
- package/src/core/ai-commit-summary.js +18 -0
- package/src/core/ai-config.js +342 -0
- package/src/core/ai-diff-range.js +117 -0
- package/src/core/args.js +47 -0
- package/src/core/commit-conventions.js +169 -0
- package/src/core/config-store.js +194 -0
- package/src/core/errors.js +25 -0
- package/src/core/git-context.js +105 -0
- package/src/core/git-service.js +428 -0
- package/src/core/hook-diagnostics.js +23 -0
- package/src/core/loading.js +36 -0
- package/src/core/logger.js +55 -0
- package/src/core/prompts.js +152 -0
- package/src/core/shell.js +77 -0
- package/src/workflows/ai-codereview-workflow.js +126 -0
- package/src/workflows/ai-commit-workflow.js +128 -0
- package/src/workflows/ai-conflict-workflow.js +102 -0
- package/src/workflows/ai-explain-workflow.js +116 -0
- package/src/workflows/ai-risk-review-workflow.js +49 -0
- package/src/workflows/checkout-workflow.js +85 -0
- package/src/workflows/clean-workflow.js +131 -0
- package/src/workflows/info-workflow.js +30 -0
- package/src/workflows/migrate-workflow.js +449 -0
- package/src/workflows/push-workflow.js +276 -0
- package/src/workflows/rollback-workflow.js +84 -0
- package/src/workflows/sync-workflow.js +141 -0
- package/src/workflows/tag-workflow.js +64 -0
- package/src/workflows/undo-workflow.js +328 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { IcodeError } from './errors.js'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
version: 1,
|
|
8
|
+
defaults: {
|
|
9
|
+
repoMode: 'auto',
|
|
10
|
+
defaultMainBranches: ['main', 'master']
|
|
11
|
+
},
|
|
12
|
+
ai: {
|
|
13
|
+
activeProfile: '',
|
|
14
|
+
profiles: {},
|
|
15
|
+
options: {}
|
|
16
|
+
},
|
|
17
|
+
repositories: {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getConfigFilePath() {
|
|
21
|
+
if (process.env.ICODE_CONFIG_PATH) {
|
|
22
|
+
return path.resolve(process.env.ICODE_CONFIG_PATH)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const homePath = os.homedir()
|
|
26
|
+
const legacyPath = path.resolve(homePath, '.icode')
|
|
27
|
+
const modernPath = path.resolve(homePath, '.icode', 'config.json')
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(legacyPath)) {
|
|
30
|
+
const legacyStats = fs.statSync(legacyPath)
|
|
31
|
+
if (legacyStats.isFile()) {
|
|
32
|
+
return legacyPath
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return modernPath
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureDirectory(filePath) {
|
|
40
|
+
const dirPath = path.dirname(filePath)
|
|
41
|
+
if (!fs.existsSync(dirPath)) {
|
|
42
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cloneDefault() {
|
|
47
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readConfig() {
|
|
51
|
+
const configPath = getConfigFilePath()
|
|
52
|
+
ensureDirectory(configPath)
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(configPath)) {
|
|
55
|
+
const initial = cloneDefault()
|
|
56
|
+
fs.writeFileSync(configPath, JSON.stringify(initial, null, 2), 'utf8')
|
|
57
|
+
return initial
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = fs.readFileSync(configPath, 'utf8')
|
|
62
|
+
if (!content.trim()) {
|
|
63
|
+
const initial = cloneDefault()
|
|
64
|
+
fs.writeFileSync(configPath, JSON.stringify(initial, null, 2), 'utf8')
|
|
65
|
+
return initial
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parsed = JSON.parse(content)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...cloneDefault(),
|
|
72
|
+
...parsed,
|
|
73
|
+
defaults: {
|
|
74
|
+
...cloneDefault().defaults,
|
|
75
|
+
...(parsed.defaults || {})
|
|
76
|
+
},
|
|
77
|
+
ai: {
|
|
78
|
+
...cloneDefault().ai,
|
|
79
|
+
...(parsed.ai || {}),
|
|
80
|
+
profiles: {
|
|
81
|
+
...(parsed.ai?.profiles || {})
|
|
82
|
+
},
|
|
83
|
+
options: {
|
|
84
|
+
...(parsed.ai?.options && typeof parsed.ai.options === 'object' && !Array.isArray(parsed.ai.options)
|
|
85
|
+
? parsed.ai.options
|
|
86
|
+
: {})
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
repositories: {
|
|
90
|
+
...(parsed.repositories || {})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new IcodeError(`配置文件解析失败: ${configPath}`, {
|
|
95
|
+
code: 'CONFIG_PARSE_ERROR',
|
|
96
|
+
cause: error
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function writeConfig(nextConfig) {
|
|
102
|
+
const configPath = getConfigFilePath()
|
|
103
|
+
ensureDirectory(configPath)
|
|
104
|
+
fs.writeFileSync(configPath, JSON.stringify(nextConfig, null, 2), 'utf8')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function splitPathSegments(pathExpression) {
|
|
108
|
+
return pathExpression
|
|
109
|
+
.split('.')
|
|
110
|
+
.map((segment) => segment.trim())
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getValue(pathExpression) {
|
|
115
|
+
const config = readConfig()
|
|
116
|
+
const segments = splitPathSegments(pathExpression)
|
|
117
|
+
|
|
118
|
+
let pointer = config
|
|
119
|
+
for (const segment of segments) {
|
|
120
|
+
if (pointer == null || typeof pointer !== 'object') {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
pointer = pointer[segment]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return pointer
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function setValue(pathExpression, value) {
|
|
130
|
+
const config = readConfig()
|
|
131
|
+
const segments = splitPathSegments(pathExpression)
|
|
132
|
+
|
|
133
|
+
if (!segments.length) {
|
|
134
|
+
throw new IcodeError('配置路径不能为空', { code: 'CONFIG_PATH_EMPTY' })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let pointer = config
|
|
138
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
139
|
+
const key = segments[index]
|
|
140
|
+
if (pointer[key] == null || typeof pointer[key] !== 'object') {
|
|
141
|
+
pointer[key] = {}
|
|
142
|
+
}
|
|
143
|
+
pointer = pointer[key]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pointer[segments[segments.length - 1]] = value
|
|
147
|
+
writeConfig(config)
|
|
148
|
+
return config
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function deleteValue(pathExpression) {
|
|
152
|
+
const config = readConfig()
|
|
153
|
+
const segments = splitPathSegments(pathExpression)
|
|
154
|
+
|
|
155
|
+
if (!segments.length) {
|
|
156
|
+
throw new IcodeError('配置路径不能为空', { code: 'CONFIG_PATH_EMPTY' })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let pointer = config
|
|
160
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
161
|
+
pointer = pointer?.[segments[index]]
|
|
162
|
+
if (pointer == null || typeof pointer !== 'object') {
|
|
163
|
+
return config
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
delete pointer[segments[segments.length - 1]]
|
|
168
|
+
writeConfig(config)
|
|
169
|
+
return config
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeRepoKey(repoRootPath) {
|
|
173
|
+
return path.resolve(repoRootPath)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getRepoPolicy(repoRootPath) {
|
|
177
|
+
const key = normalizeRepoKey(repoRootPath)
|
|
178
|
+
const config = readConfig()
|
|
179
|
+
return config.repositories[key] || {
|
|
180
|
+
protectedBranches: []
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function setRepoPolicy(repoRootPath, policy) {
|
|
185
|
+
const key = normalizeRepoKey(repoRootPath)
|
|
186
|
+
const config = readConfig()
|
|
187
|
+
config.repositories[key] = {
|
|
188
|
+
protectedBranches: [],
|
|
189
|
+
...(config.repositories[key] || {}),
|
|
190
|
+
...policy
|
|
191
|
+
}
|
|
192
|
+
writeConfig(config)
|
|
193
|
+
return config.repositories[key]
|
|
194
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class IcodeError extends Error {
|
|
2
|
+
constructor(message, options = {}) {
|
|
3
|
+
super(message)
|
|
4
|
+
this.name = 'IcodeError'
|
|
5
|
+
this.code = options.code || 'ICODE_ERROR'
|
|
6
|
+
this.exitCode = options.exitCode || 1
|
|
7
|
+
this.meta = options.meta || {}
|
|
8
|
+
this.cause = options.cause
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function asIcodeError(error, fallbackMessage = '命令执行失败') {
|
|
13
|
+
if (error instanceof IcodeError) {
|
|
14
|
+
return error
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const wrapped = new IcodeError(error?.message || fallbackMessage, {
|
|
18
|
+
cause: error,
|
|
19
|
+
meta: {
|
|
20
|
+
original: error
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return wrapped
|
|
25
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { readConfig } from './config-store.js'
|
|
4
|
+
import { IcodeError } from './errors.js'
|
|
5
|
+
import { runCommand } from './shell.js'
|
|
6
|
+
|
|
7
|
+
function cleanOutput(text) {
|
|
8
|
+
return (text || '').trim()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function commandOutput(cwd, args, allowFailure = false) {
|
|
12
|
+
const result = await runCommand('git', args, { cwd, allowFailure })
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function detectDefaultBranch(topLevelPath, fallbackCandidates = ['main', 'master']) {
|
|
17
|
+
const headRefResult = await commandOutput(topLevelPath, ['symbolic-ref', 'refs/remotes/origin/HEAD'], true)
|
|
18
|
+
if (headRefResult.exitCode === 0) {
|
|
19
|
+
const headRef = cleanOutput(headRefResult.stdout)
|
|
20
|
+
const branchName = headRef.replace('refs/remotes/origin/', '')
|
|
21
|
+
if (branchName) {
|
|
22
|
+
return branchName
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const candidate of fallbackCandidates) {
|
|
27
|
+
const localRef = await commandOutput(topLevelPath, ['show-ref', '--verify', '--quiet', `refs/heads/${candidate}`], true)
|
|
28
|
+
if (localRef.exitCode === 0) {
|
|
29
|
+
return candidate
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const remoteRef = await commandOutput(topLevelPath, ['show-ref', '--verify', '--quiet', `refs/remotes/origin/${candidate}`], true)
|
|
33
|
+
if (remoteRef.exitCode === 0) {
|
|
34
|
+
return candidate
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return 'main'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resolveGitContext(options = {}) {
|
|
42
|
+
const cwd = path.resolve(options.cwd || process.cwd())
|
|
43
|
+
const config = readConfig()
|
|
44
|
+
const configRepoMode = config.defaults?.repoMode || 'auto'
|
|
45
|
+
const repoMode = options.repoMode || configRepoMode
|
|
46
|
+
|
|
47
|
+
const inside = await commandOutput(cwd, ['rev-parse', '--is-inside-work-tree'], true)
|
|
48
|
+
if (inside.exitCode !== 0 || cleanOutput(inside.stdout) !== 'true') {
|
|
49
|
+
throw new IcodeError('当前目录不在 Git 仓库中。', {
|
|
50
|
+
code: 'NOT_IN_GIT_REPO',
|
|
51
|
+
exitCode: 2
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const topLevelPath = cleanOutput((await commandOutput(cwd, ['rev-parse', '--show-toplevel'])).stdout)
|
|
56
|
+
const gitDirRaw = cleanOutput((await commandOutput(cwd, ['rev-parse', '--git-dir'])).stdout)
|
|
57
|
+
const commonDirRaw = cleanOutput((await commandOutput(cwd, ['rev-parse', '--git-common-dir'])).stdout)
|
|
58
|
+
const currentBranch = cleanOutput((await commandOutput(topLevelPath, ['branch', '--show-current'], true)).stdout)
|
|
59
|
+
const superproject = cleanOutput((await commandOutput(cwd, ['rev-parse', '--show-superproject-working-tree'], true)).stdout)
|
|
60
|
+
|
|
61
|
+
const inheritedFromParent = path.resolve(cwd) !== path.resolve(topLevelPath)
|
|
62
|
+
// strict 模式用于“防误操作”场景:如果当前目录只是父仓库的子目录,直接阻断。
|
|
63
|
+
if (repoMode === 'strict' && inheritedFromParent) {
|
|
64
|
+
throw new IcodeError(
|
|
65
|
+
`检测到父级仓库继承: 当前目录 ${cwd} 实际仓库根目录为 ${topLevelPath}。strict 模式已阻止继续执行。`,
|
|
66
|
+
{
|
|
67
|
+
code: 'PARENT_REPO_INHERITED',
|
|
68
|
+
exitCode: 2
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const gitDir = path.isAbsolute(gitDirRaw) ? gitDirRaw : path.resolve(topLevelPath, gitDirRaw)
|
|
74
|
+
const commonDir = path.isAbsolute(commonDirRaw) ? commonDirRaw : path.resolve(topLevelPath, commonDirRaw)
|
|
75
|
+
|
|
76
|
+
const configuredHookPath = cleanOutput((await commandOutput(topLevelPath, ['config', '--get', 'core.hooksPath'], true)).stdout)
|
|
77
|
+
// hooksPath 可能是相对路径,也可能是绝对路径,统一转成绝对路径便于后续检测。
|
|
78
|
+
const hookPath = configuredHookPath
|
|
79
|
+
? (path.isAbsolute(configuredHookPath) ? configuredHookPath : path.resolve(topLevelPath, configuredHookPath))
|
|
80
|
+
: path.resolve(gitDir, 'hooks')
|
|
81
|
+
|
|
82
|
+
const hasHuskyFolder = fs.existsSync(path.resolve(topLevelPath, '.husky'))
|
|
83
|
+
const hasHookPath = fs.existsSync(hookPath)
|
|
84
|
+
|
|
85
|
+
const defaultBranch = await detectDefaultBranch(
|
|
86
|
+
topLevelPath,
|
|
87
|
+
config.defaults?.defaultMainBranches || ['main', 'master']
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
cwd,
|
|
92
|
+
repoMode,
|
|
93
|
+
topLevelPath,
|
|
94
|
+
gitDir,
|
|
95
|
+
commonDir,
|
|
96
|
+
currentBranch,
|
|
97
|
+
defaultBranch,
|
|
98
|
+
inheritedFromParent,
|
|
99
|
+
hasHuskyFolder,
|
|
100
|
+
hookPath,
|
|
101
|
+
hasHookPath,
|
|
102
|
+
isSubmodule: Boolean(superproject),
|
|
103
|
+
superprojectPath: superproject || null
|
|
104
|
+
}
|
|
105
|
+
}
|