@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.
Files changed (46) hide show
  1. package/README.md +346 -0
  2. package/bin/icode.js +6 -0
  3. package/package.json +34 -0
  4. package/src/cli.js +131 -0
  5. package/src/commands/ai.js +287 -0
  6. package/src/commands/checkout.js +59 -0
  7. package/src/commands/clean.js +65 -0
  8. package/src/commands/codereview.js +52 -0
  9. package/src/commands/config.js +513 -0
  10. package/src/commands/explain.js +80 -0
  11. package/src/commands/help.js +49 -0
  12. package/src/commands/info.js +57 -0
  13. package/src/commands/migrate.js +86 -0
  14. package/src/commands/push.js +125 -0
  15. package/src/commands/sync.js +74 -0
  16. package/src/commands/tag.js +53 -0
  17. package/src/commands/undo.js +66 -0
  18. package/src/core/ai-client.js +1125 -0
  19. package/src/core/ai-commit-summary.js +18 -0
  20. package/src/core/ai-config.js +342 -0
  21. package/src/core/ai-diff-range.js +117 -0
  22. package/src/core/args.js +47 -0
  23. package/src/core/commit-conventions.js +169 -0
  24. package/src/core/config-store.js +194 -0
  25. package/src/core/errors.js +25 -0
  26. package/src/core/git-context.js +105 -0
  27. package/src/core/git-service.js +428 -0
  28. package/src/core/hook-diagnostics.js +23 -0
  29. package/src/core/loading.js +36 -0
  30. package/src/core/logger.js +55 -0
  31. package/src/core/prompts.js +152 -0
  32. package/src/core/shell.js +77 -0
  33. package/src/workflows/ai-codereview-workflow.js +126 -0
  34. package/src/workflows/ai-commit-workflow.js +128 -0
  35. package/src/workflows/ai-conflict-workflow.js +102 -0
  36. package/src/workflows/ai-explain-workflow.js +116 -0
  37. package/src/workflows/ai-risk-review-workflow.js +49 -0
  38. package/src/workflows/checkout-workflow.js +85 -0
  39. package/src/workflows/clean-workflow.js +131 -0
  40. package/src/workflows/info-workflow.js +30 -0
  41. package/src/workflows/migrate-workflow.js +449 -0
  42. package/src/workflows/push-workflow.js +276 -0
  43. package/src/workflows/rollback-workflow.js +84 -0
  44. package/src/workflows/sync-workflow.js +141 -0
  45. package/src/workflows/tag-workflow.js +64 -0
  46. 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
+ }