@icode-js/icode 3.0.4 → 3.1.0
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 +21 -0
- package/README.md +40 -3
- package/bin/icode.js +4 -4
- package/package.json +1 -1
- package/src/cli.js +54 -23
- package/src/commands/ai.js +4 -4
- package/src/commands/checkout.js +2 -2
- package/src/commands/clean.js +1 -1
- package/src/commands/codereview.js +1 -1
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +117 -7
- package/src/commands/explain.js +2 -2
- package/src/commands/help.js +7 -0
- package/src/commands/info.js +1 -1
- package/src/commands/migrate.js +2 -2
- package/src/commands/push.js +29 -8
- package/src/commands/sync.js +2 -2
- package/src/commands/tag.js +2 -2
- package/src/commands/undo.js +60 -9
- package/src/core/{ai-client.js → ai/client.js} +3 -3
- package/src/core/{ai-config.js → ai/config.js} +2 -2
- package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
- package/src/core/cli/completion.js +687 -0
- package/src/core/cli/update-notifier.js +138 -0
- package/src/core/{shell.js → commands/shell.js} +13 -1
- package/src/core/{config-store.js → config/store.js} +55 -3
- package/src/core/{git-context.js → git/context.js} +25 -10
- package/src/core/{git-service.js → git/service.js} +44 -4
- package/src/core/remote-merge/client.js +42 -0
- package/src/core/remote-merge/providers/gitlab.js +537 -0
- package/src/core/remote-merge/shared.js +49 -0
- package/src/core/{prompts.js → tools/interactive.js} +46 -5
- package/src/workflows/ai-codereview-workflow.js +4 -4
- package/src/workflows/ai-commit-workflow.js +6 -6
- package/src/workflows/ai-conflict-workflow.js +4 -4
- package/src/workflows/ai-explain-workflow.js +4 -4
- package/src/workflows/ai-risk-review-workflow.js +1 -1
- package/src/workflows/checkout-workflow.js +3 -3
- package/src/workflows/clean-workflow.js +5 -5
- package/src/workflows/info-workflow.js +4 -4
- package/src/workflows/migrate-workflow.js +4 -4
- package/src/workflows/push-workflow.js +259 -24
- package/src/workflows/rollback-workflow.js +4 -4
- package/src/workflows/sync-workflow.js +4 -4
- package/src/workflows/tag-workflow.js +4 -4
- package/src/workflows/undo-workflow.js +12 -5
- /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
- /package/src/core/{args.js → cli/args.js} +0 -0
- /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
- /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
- /package/src/core/{loading.js → tools/loading.js} +0 -0
- /package/src/core/{logger.js → tools/logger.js} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { runCommand } from '../commands/shell.js'
|
|
3
|
+
import { logger } from '../tools/logger.js'
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url)
|
|
6
|
+
const packageJson = require('../../../package.json')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 规范化版本号,输出仅包含数字段的 semver 主体数组。
|
|
10
|
+
* 输入为字符串版本号,输出为数字数组;若格式不合法则返回 null。
|
|
11
|
+
*/
|
|
12
|
+
function normalizeVersionSegments(version) {
|
|
13
|
+
const normalized = String(version || '').trim()
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const coreVersion = normalized.split('-')[0]
|
|
19
|
+
if (!/^\d+(?:\.\d+)*$/.test(coreVersion)) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return coreVersion.split('.').map((segment) => Number(segment))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 解析 npm view 返回的版本文本,兼容 JSON 字符串与纯文本两种输出。
|
|
28
|
+
* 输入为命令 stdout,输出为去空白后的版本号字符串。
|
|
29
|
+
*/
|
|
30
|
+
function parsePublishedVersion(stdout) {
|
|
31
|
+
const normalized = String(stdout || '').trim()
|
|
32
|
+
if (!normalized) {
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(normalized)
|
|
38
|
+
if (typeof parsed === 'string') {
|
|
39
|
+
return parsed.trim()
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// npm 在异常场景下也可能输出纯文本版本号,这里保留原始文本继续解析。
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalized.replace(/^"|"$/g, '').trim()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 比较两个版本号,判断远端版本是否严格高于当前版本。
|
|
50
|
+
* 输入为当前版本和远端版本,输出为布尔值。
|
|
51
|
+
*/
|
|
52
|
+
export function hasNewerVersion(currentVersion, latestVersion) {
|
|
53
|
+
const currentSegments = normalizeVersionSegments(currentVersion)
|
|
54
|
+
const latestSegments = normalizeVersionSegments(latestVersion)
|
|
55
|
+
|
|
56
|
+
if (!currentSegments || !latestSegments) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const maxLength = Math.max(currentSegments.length, latestSegments.length)
|
|
61
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
62
|
+
const currentValue = currentSegments[index] || 0
|
|
63
|
+
const latestValue = latestSegments[index] || 0
|
|
64
|
+
|
|
65
|
+
if (latestValue > currentValue) {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (latestValue < currentValue) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 查询 npm registry 上当前包的最新发布版本。
|
|
79
|
+
* 输入为可选 runner/packageName,输出为最新版本号;失败时返回空字符串。
|
|
80
|
+
*/
|
|
81
|
+
export async function fetchLatestPublishedVersion(options = {}) {
|
|
82
|
+
const packageName = options.packageName || packageJson.name
|
|
83
|
+
const runner = options.runner || runCommand
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await runner('npm', ['view', packageName, 'version', '--json'], {
|
|
87
|
+
allowFailure: true,
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
NO_UPDATE_NOTIFIER: '1',
|
|
91
|
+
npm_config_update_notifier: 'false',
|
|
92
|
+
npm_config_loglevel: 'silent'
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (result.exitCode !== 0) {
|
|
97
|
+
return ''
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parsePublishedVersion(result.stdout)
|
|
101
|
+
} catch {
|
|
102
|
+
return ''
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 在命令成功结束后提示可用更新版本。
|
|
108
|
+
* 输入为可选 runner/warn/packageName/currentVersion,输出为通知结果对象。
|
|
109
|
+
*/
|
|
110
|
+
export async function notifyIfCliUpdateAvailable(options = {}) {
|
|
111
|
+
const packageName = options.packageName || packageJson.name
|
|
112
|
+
const currentVersion = options.currentVersion || packageJson.version
|
|
113
|
+
const warn = options.warn || logger.warn
|
|
114
|
+
|
|
115
|
+
const latestVersion = await fetchLatestPublishedVersion({
|
|
116
|
+
runner: options.runner,
|
|
117
|
+
packageName
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (!latestVersion || !hasNewerVersion(currentVersion, latestVersion)) {
|
|
121
|
+
return {
|
|
122
|
+
notified: false,
|
|
123
|
+
packageName,
|
|
124
|
+
currentVersion,
|
|
125
|
+
latestVersion
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
warn(`发现新版本可用: ${currentVersion} -> ${latestVersion}`)
|
|
130
|
+
warn(`更新命令: npm i -g ${packageName}@latest`)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
notified: true,
|
|
134
|
+
packageName,
|
|
135
|
+
currentVersion,
|
|
136
|
+
latestVersion
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import { IcodeError } from '
|
|
2
|
+
import { IcodeError } from '../errors.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 把命令与参数拼成可读字符串,供日志与报错复用。
|
|
6
|
+
* 输入为命令名与参数数组,输出为单行命令文本。
|
|
7
|
+
*/
|
|
4
8
|
function stringifyCommand(command, args) {
|
|
5
9
|
return [command, ...args].join(' ')
|
|
6
10
|
}
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 组合命令失败消息,尽量附带 stderr/stdout 便于定位问题。
|
|
14
|
+
* 输入为命令信息与执行结果,输出为最终错误文案。
|
|
15
|
+
*/
|
|
8
16
|
function formatFailureMessage(command, args, result) {
|
|
9
17
|
const baseMessage = `命令执行失败(${result.exitCode}): ${stringifyCommand(command, args)}`
|
|
10
18
|
const detailLines = [result.stderr, result.stdout].map((item) => item.trim()).filter(Boolean)
|
|
@@ -16,6 +24,10 @@ function formatFailureMessage(command, args, result) {
|
|
|
16
24
|
return `${baseMessage}\n${detailLines.join('\n')}`
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 执行外部命令并收集 stdout/stderr;允许按需把非零退出码作为普通结果返回。
|
|
29
|
+
* 输入为命令名、参数数组与执行选项,输出为标准化执行结果。
|
|
30
|
+
*/
|
|
19
31
|
export async function runCommand(command, args = [], options = {}) {
|
|
20
32
|
const { cwd = process.cwd(), env = process.env, allowFailure = false } = options
|
|
21
33
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import { IcodeError } from '
|
|
4
|
+
import { IcodeError } from '../errors.js'
|
|
5
5
|
|
|
6
6
|
const DEFAULT_CONFIG = {
|
|
7
7
|
version: 1,
|
|
@@ -9,6 +9,12 @@ const DEFAULT_CONFIG = {
|
|
|
9
9
|
repoMode: 'auto',
|
|
10
10
|
defaultMainBranches: ['main', 'master']
|
|
11
11
|
},
|
|
12
|
+
platforms: {
|
|
13
|
+
remoteMerge: {
|
|
14
|
+
provider: '',
|
|
15
|
+
apiKey: ''
|
|
16
|
+
}
|
|
17
|
+
},
|
|
12
18
|
ai: {
|
|
13
19
|
activeProfile: '',
|
|
14
20
|
profiles: {},
|
|
@@ -74,6 +80,14 @@ export function readConfig() {
|
|
|
74
80
|
...cloneDefault().defaults,
|
|
75
81
|
...(parsed.defaults || {})
|
|
76
82
|
},
|
|
83
|
+
platforms: {
|
|
84
|
+
...cloneDefault().platforms,
|
|
85
|
+
...(parsed.platforms || {}),
|
|
86
|
+
remoteMerge: {
|
|
87
|
+
...cloneDefault().platforms.remoteMerge,
|
|
88
|
+
...(parsed.platforms?.remoteMerge || {})
|
|
89
|
+
}
|
|
90
|
+
},
|
|
77
91
|
ai: {
|
|
78
92
|
...cloneDefault().ai,
|
|
79
93
|
...(parsed.ai || {}),
|
|
@@ -176,8 +190,13 @@ function normalizeRepoKey(repoRootPath) {
|
|
|
176
190
|
export function getRepoPolicy(repoRootPath) {
|
|
177
191
|
const key = normalizeRepoKey(repoRootPath)
|
|
178
192
|
const config = readConfig()
|
|
179
|
-
return
|
|
180
|
-
protectedBranches: []
|
|
193
|
+
return {
|
|
194
|
+
protectedBranches: [],
|
|
195
|
+
remoteMerge: {
|
|
196
|
+
enabled: true,
|
|
197
|
+
projectUrl: ''
|
|
198
|
+
},
|
|
199
|
+
...(config.repositories[key] || {})
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
|
|
@@ -186,9 +205,42 @@ export function setRepoPolicy(repoRootPath, policy) {
|
|
|
186
205
|
const config = readConfig()
|
|
187
206
|
config.repositories[key] = {
|
|
188
207
|
protectedBranches: [],
|
|
208
|
+
remoteMerge: {
|
|
209
|
+
enabled: true,
|
|
210
|
+
projectUrl: ''
|
|
211
|
+
},
|
|
189
212
|
...(config.repositories[key] || {}),
|
|
190
213
|
...policy
|
|
191
214
|
}
|
|
192
215
|
writeConfig(config)
|
|
193
216
|
return config.repositories[key]
|
|
194
217
|
}
|
|
218
|
+
|
|
219
|
+
export function getPlatformConfig(platformName) {
|
|
220
|
+
const config = readConfig()
|
|
221
|
+
const name = String(platformName || '').trim()
|
|
222
|
+
if (!name) {
|
|
223
|
+
return {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return config.platforms?.[name] || {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function setPlatformConfig(platformName, value) {
|
|
230
|
+
const config = readConfig()
|
|
231
|
+
const name = String(platformName || '').trim()
|
|
232
|
+
if (!name) {
|
|
233
|
+
throw new IcodeError('平台名称不能为空', { code: 'CONFIG_PLATFORM_EMPTY', exitCode: 2 })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
config.platforms = {
|
|
237
|
+
...(config.platforms || {}),
|
|
238
|
+
[name]: {
|
|
239
|
+
...(config.platforms?.[name] || {}),
|
|
240
|
+
...(value || {})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
writeConfig(config)
|
|
245
|
+
return config.platforms[name]
|
|
246
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { runCommand } from '../commands/shell.js'
|
|
4
|
+
import { readConfig } from '../config/store.js'
|
|
5
|
+
import { IcodeError } from '../errors.js'
|
|
6
|
+
import { confirm } from '../tools/interactive.js'
|
|
6
7
|
|
|
7
8
|
function cleanOutput(text) {
|
|
8
9
|
return (text || '').trim()
|
|
@@ -13,6 +14,24 @@ async function commandOutput(cwd, args, allowFailure = false) {
|
|
|
13
14
|
return result
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
async function ensureGitRepository(cwd, options = {}) {
|
|
18
|
+
const confirmInit = options.confirm || confirm
|
|
19
|
+
const inside = await commandOutput(cwd, ['rev-parse', '--is-inside-work-tree'], true)
|
|
20
|
+
if (inside.exitCode === 0 && cleanOutput(inside.stdout) === 'true') {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const shouldInit = await confirmInit('当前目录还没有初始化 Git 仓库,是否现在执行 git init?')
|
|
25
|
+
if (!shouldInit) {
|
|
26
|
+
throw new IcodeError('当前目录不在 Git 仓库中。', {
|
|
27
|
+
code: 'NOT_IN_GIT_REPO',
|
|
28
|
+
exitCode: 2
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await commandOutput(cwd, ['init'])
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
async function detectDefaultBranch(topLevelPath, fallbackCandidates = ['main', 'master']) {
|
|
17
36
|
const headRefResult = await commandOutput(topLevelPath, ['symbolic-ref', 'refs/remotes/origin/HEAD'], true)
|
|
18
37
|
if (headRefResult.exitCode === 0) {
|
|
@@ -44,13 +63,9 @@ export async function resolveGitContext(options = {}) {
|
|
|
44
63
|
const configRepoMode = config.defaults?.repoMode || 'auto'
|
|
45
64
|
const repoMode = options.repoMode || configRepoMode
|
|
46
65
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
code: 'NOT_IN_GIT_REPO',
|
|
51
|
-
exitCode: 2
|
|
52
|
-
})
|
|
53
|
-
}
|
|
66
|
+
await ensureGitRepository(cwd, {
|
|
67
|
+
confirm: options.confirm
|
|
68
|
+
})
|
|
54
69
|
|
|
55
70
|
const topLevelPath = cleanOutput((await commandOutput(cwd, ['rev-parse', '--show-toplevel'])).stdout)
|
|
56
71
|
const gitDirRaw = cleanOutput((await commandOutput(cwd, ['rev-parse', '--git-dir'])).stdout)
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import { IcodeError } from './errors.js'
|
|
2
|
-
import { buildHookHint, detectHookFailure } from './hook-diagnostics.js'
|
|
3
|
-
import { logger } from './logger.js'
|
|
4
|
-
import { runCommand } from './shell.js'
|
|
5
1
|
import fs from 'node:fs'
|
|
6
2
|
import path from 'node:path'
|
|
3
|
+
import { buildHookHint, detectHookFailure } from './hook-diagnostics.js'
|
|
4
|
+
import { runCommand } from '../commands/shell.js'
|
|
5
|
+
import { IcodeError } from '../errors.js'
|
|
6
|
+
import { logger } from '../tools/logger.js'
|
|
7
7
|
|
|
8
8
|
function cleanOutput(text) {
|
|
9
9
|
return (text || '').trim()
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function parseGitLogRecords(output) {
|
|
13
|
+
return output
|
|
14
|
+
.split('\x1e')
|
|
15
|
+
.map((record) => record.trim())
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((record) => {
|
|
18
|
+
const [hash = '', subject = '', body = ''] = record.split('\x1f')
|
|
19
|
+
return {
|
|
20
|
+
hash: hash.trim(),
|
|
21
|
+
subject: subject.trim(),
|
|
22
|
+
body: body.trim()
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.filter((item) => item.hash && item.subject)
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export class GitService {
|
|
13
29
|
constructor(context) {
|
|
14
30
|
this.context = context
|
|
@@ -77,6 +93,13 @@ export class GitService {
|
|
|
77
93
|
return result.exitCode === 0
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
async getRemoteUrl(remoteName = 'origin') {
|
|
97
|
+
const result = await this.exec(['remote', 'get-url', remoteName], {
|
|
98
|
+
allowFailure: true
|
|
99
|
+
})
|
|
100
|
+
return cleanOutput(result.stdout)
|
|
101
|
+
}
|
|
102
|
+
|
|
80
103
|
async listLocalBranches() {
|
|
81
104
|
const result = await this.exec(['for-each-ref', '--format=%(refname:short)', 'refs/heads'])
|
|
82
105
|
return result.stdout
|
|
@@ -431,6 +454,23 @@ export class GitService {
|
|
|
431
454
|
.filter(Boolean)
|
|
432
455
|
}
|
|
433
456
|
|
|
457
|
+
// 读取指定 range 内的提交信息,返回按时间顺序排列的 hash/subject/body 列表。
|
|
458
|
+
async listCommitMessages(rangeSpec) {
|
|
459
|
+
const result = await this.exec(['log', '--reverse', '--format=%H%x1f%s%x1f%b%x1e', rangeSpec], {
|
|
460
|
+
allowFailure: true
|
|
461
|
+
})
|
|
462
|
+
return parseGitLogRecords(result.stdout)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 读取单个 ref 的最新提交信息,供无法从 range 提取提交时回退使用。
|
|
466
|
+
async getCommitMessage(ref = 'HEAD') {
|
|
467
|
+
const result = await this.exec(['log', '-1', '--format=%H%x1f%s%x1f%b%x1e', ref], {
|
|
468
|
+
allowFailure: true
|
|
469
|
+
})
|
|
470
|
+
const commits = parseGitLogRecords(result.stdout)
|
|
471
|
+
return commits[0] || null
|
|
472
|
+
}
|
|
473
|
+
|
|
434
474
|
async cherryPick(commits) {
|
|
435
475
|
const normalized = Array.isArray(commits) ? commits : [commits]
|
|
436
476
|
const list = normalized.map((item) => item.trim()).filter(Boolean)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { IcodeError } from '../errors.js'
|
|
2
|
+
import { requestGitLabRemoteMerge, resolveGitLabProjectUrlFromRemote } from './providers/gitlab.js'
|
|
3
|
+
|
|
4
|
+
const REMOTE_MERGE_PROVIDERS = {
|
|
5
|
+
gitlab: {
|
|
6
|
+
requestRemoteMerge: requestGitLabRemoteMerge,
|
|
7
|
+
resolveProjectUrlFromRemote: resolveGitLabProjectUrlFromRemote
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 按 provider 读取远程合并实现,缺失或不支持时统一抛出明确错误。
|
|
12
|
+
function getRemoteMergeProvider(provider) {
|
|
13
|
+
const normalizedProvider = String(provider || '').trim()
|
|
14
|
+
if (!normalizedProvider) {
|
|
15
|
+
throw new IcodeError('远程合并缺少 provider', {
|
|
16
|
+
code: 'REMOTE_MERGE_PROVIDER_EMPTY',
|
|
17
|
+
exitCode: 2
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handler = REMOTE_MERGE_PROVIDERS[normalizedProvider]
|
|
22
|
+
if (!handler) {
|
|
23
|
+
throw new IcodeError(`当前仅支持: ${Object.keys(REMOTE_MERGE_PROVIDERS).join(', ')},收到 provider=${normalizedProvider}`, {
|
|
24
|
+
code: 'REMOTE_MERGE_PROVIDER_UNSUPPORTED',
|
|
25
|
+
exitCode: 2
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return handler
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 统一根据 provider 从 remote URL 解析项目地址,避免工作流层感知平台细节。
|
|
33
|
+
export function resolveRemoteMergeProjectUrl(options = {}) {
|
|
34
|
+
const handler = getRemoteMergeProvider(options.provider)
|
|
35
|
+
return handler.resolveProjectUrlFromRemote(options.remoteUrl || '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 统一按 provider 调度远程合并请求,后续新增平台只需补 provider 实现。
|
|
39
|
+
export async function requestRemoteMerge(options = {}) {
|
|
40
|
+
const handler = getRemoteMergeProvider(options.provider)
|
|
41
|
+
return handler.requestRemoteMerge(options)
|
|
42
|
+
}
|