@icode-js/icode 3.2.0 → 3.4.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/README.md +287 -24
- package/bin/icode.js +10 -0
- package/package.json +3 -3
- package/src/cli.js +175 -8
- package/src/commands/checkout.js +73 -6
- package/src/commands/clean.js +47 -7
- package/src/commands/config-ux.js +292 -0
- package/src/commands/config.js +199 -48
- package/src/commands/continue.js +104 -0
- package/src/commands/doctor.js +15 -29
- package/src/commands/help.js +54 -34
- package/src/commands/history.js +147 -0
- package/src/commands/info.js +39 -4
- package/src/commands/log.js +142 -0
- package/src/commands/migrate.js +19 -12
- package/src/commands/open.js +138 -0
- package/src/commands/pr.js +219 -0
- package/src/commands/push.js +62 -22
- package/src/commands/ready.js +18 -48
- package/src/commands/release.js +112 -0
- package/src/commands/rescue.js +164 -0
- package/src/commands/save.js +52 -11
- package/src/commands/stash.js +210 -0
- package/src/commands/sw.js +89 -0
- package/src/commands/sync.js +19 -2
- package/src/commands/tag.js +40 -1
- package/src/commands/undo.js +9 -3
- package/src/core/ai/diff-range.js +1 -1
- package/src/core/cli/completion.js +237 -1
- package/src/core/cli/output.js +46 -0
- package/src/core/cli/render.js +121 -0
- package/src/core/commands/shell.js +9 -1
- package/src/core/config/repo-config.js +93 -0
- package/src/core/config/store.js +60 -10
- package/src/core/git/autostash.js +46 -0
- package/src/core/git/backup.js +170 -0
- package/src/core/git/conflicts.js +58 -0
- package/src/core/git/context.js +72 -5
- package/src/core/git/file-guard.js +193 -0
- package/src/core/git/operation-state.js +63 -0
- package/src/core/git/operations.js +32 -0
- package/src/core/git/semver.js +194 -0
- package/src/core/git/service.js +454 -24
- package/src/core/platform/http.js +53 -0
- package/src/core/platform/index.js +165 -0
- package/src/core/platform/providers.js +454 -0
- package/src/core/platform/remote-url.js +92 -0
- package/src/core/remote-merge/client.js +33 -3
- package/src/core/tools/browser.js +28 -0
- package/src/core/tools/commit-prompt.js +137 -0
- package/src/core/tools/interactive.js +272 -40
- package/src/core/tools/logger.js +33 -23
- package/src/core/tools/theme.js +228 -0
- package/src/workflows/ai-commit-workflow.js +52 -20
- package/src/workflows/checkout-workflow.js +24 -10
- package/src/workflows/clean-workflow.js +135 -83
- package/src/workflows/continue-workflow.js +233 -0
- package/src/workflows/doctor-workflow.js +10 -7
- package/src/workflows/history-workflow.js +211 -0
- package/src/workflows/migrate-workflow.js +39 -14
- package/src/workflows/pr-workflow.js +252 -0
- package/src/workflows/push/commit-step.js +123 -0
- package/src/workflows/push/local-merge-step.js +213 -0
- package/src/workflows/push/plan.js +63 -0
- package/src/workflows/push/remote-merge-step.js +224 -0
- package/src/workflows/push-workflow.js +185 -492
- package/src/workflows/ready-workflow.js +14 -10
- package/src/workflows/release-workflow.js +189 -0
- package/src/workflows/rescue-workflow.js +137 -0
- package/src/workflows/save-workflow.js +106 -41
- package/src/workflows/switch-workflow.js +171 -0
- package/src/workflows/sync-workflow.js +143 -44
- package/src/workflows/tag-workflow.js +103 -17
- package/src/workflows/undo-workflow.js +113 -34
- package/src/workflows/rollback-workflow.js +0 -99
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline/promises'
|
|
2
2
|
import { stdin, stdout } from 'node:process'
|
|
3
|
+
import { c, stripAnsi } from './theme.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* 判断当前 stdin/stdout 是否连接到交互式终端。
|
|
@@ -33,9 +34,71 @@ function resolveConfirmDefaultValue(optionsOrDefaultValue) {
|
|
|
33
34
|
return false
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// 用户按 Ctrl+C 时恢复终端并退出,和常见 CLI 行为一致。
|
|
38
|
+
function exitOnCancel() {
|
|
39
|
+
stdout.write(`\n${c.gray('已取消')}\n`)
|
|
40
|
+
process.exit(130)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function questionPrefix(message) {
|
|
44
|
+
return `${c.green('?')} ${c.bold(message)}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function supportsRawMode() {
|
|
48
|
+
return typeof stdin.setRawMode === 'function'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 进入 raw 模式逐键读取输入,onKey 返回 true 表示结束。
|
|
53
|
+
* 输入为按键处理函数,输出为 Promise(结束时 resolve)。
|
|
54
|
+
*/
|
|
55
|
+
function readKeys(onKey) {
|
|
56
|
+
const wasRaw = stdin.isRaw === true
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
let done = false
|
|
59
|
+
const finish = () => {
|
|
60
|
+
if (done) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
done = true
|
|
64
|
+
stdin.off('data', onData)
|
|
65
|
+
stdin.setRawMode(wasRaw)
|
|
66
|
+
stdin.pause()
|
|
67
|
+
resolve()
|
|
68
|
+
}
|
|
69
|
+
function onData(chunk) {
|
|
70
|
+
const text = chunk.toString('utf8')
|
|
71
|
+
const keys = []
|
|
72
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
73
|
+
const rest = text.slice(index)
|
|
74
|
+
const escape = rest.match(/^\x1b\[[0-9;]*[A-Za-z~]/)
|
|
75
|
+
if (escape) {
|
|
76
|
+
keys.push(escape[0])
|
|
77
|
+
index += escape[0].length - 1
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
keys.push(text[index])
|
|
81
|
+
}
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
if (key === '\u0003') {
|
|
84
|
+
stdin.setRawMode(wasRaw)
|
|
85
|
+
exitOnCancel()
|
|
86
|
+
}
|
|
87
|
+
if (onKey(key) === true) {
|
|
88
|
+
finish()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
stdin.setRawMode(true)
|
|
94
|
+
stdin.resume()
|
|
95
|
+
stdin.on('data', onData)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
36
99
|
/**
|
|
37
100
|
* 读取确认输入并返回布尔结果;非交互环境直接返回默认值。
|
|
38
|
-
*
|
|
101
|
+
* 交互终端中单键作答:y / n,回车取默认值。
|
|
39
102
|
*/
|
|
40
103
|
export async function confirm(message, optionsOrDefaultValue = false) {
|
|
41
104
|
const defaultValue = resolveConfirmDefaultValue(optionsOrDefaultValue)
|
|
@@ -43,24 +106,46 @@ export async function confirm(message, optionsOrDefaultValue = false) {
|
|
|
43
106
|
return defaultValue
|
|
44
107
|
}
|
|
45
108
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
109
|
+
const hint = c.gray(defaultValue ? '(Y/n)' : '(y/N)')
|
|
110
|
+
if (!supportsRawMode()) {
|
|
111
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
112
|
+
const answer = (await rl.question(`${questionPrefix(message)} ${hint} `)).trim().toLowerCase()
|
|
113
|
+
rl.close()
|
|
114
|
+
if (!answer) {
|
|
115
|
+
return defaultValue
|
|
116
|
+
}
|
|
117
|
+
if (['y', 'yes', '1', 'true', 'ok', 'sure', '是'].includes(answer)) {
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
if (['n', 'no', '0', 'false', '否'].includes(answer)) {
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
52
123
|
return defaultValue
|
|
53
124
|
}
|
|
54
125
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
126
|
+
stdout.write(`${questionPrefix(message)} ${hint} `)
|
|
127
|
+
let result = defaultValue
|
|
128
|
+
await readKeys((key) => {
|
|
129
|
+
const lower = key.toLowerCase()
|
|
130
|
+
if (lower === 'y') {
|
|
131
|
+
result = true
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
if (lower === 'n') {
|
|
135
|
+
result = false
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
if (key === '\r' || key === '\n') {
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
if (key === '\x1b') {
|
|
142
|
+
result = false
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
60
145
|
return false
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return
|
|
146
|
+
})
|
|
147
|
+
stdout.write(`${result ? c.green('是') : c.gray('否')}\n`)
|
|
148
|
+
return result
|
|
64
149
|
}
|
|
65
150
|
|
|
66
151
|
/**
|
|
@@ -73,45 +158,170 @@ export async function input(message, defaultValue = '') {
|
|
|
73
158
|
}
|
|
74
159
|
|
|
75
160
|
const rl = createInterface({ input: stdin, output: stdout })
|
|
76
|
-
|
|
161
|
+
rl.on('SIGINT', () => {
|
|
162
|
+
rl.close()
|
|
163
|
+
exitOnCancel()
|
|
164
|
+
})
|
|
165
|
+
const answer = await rl.question(`${questionPrefix(message)}${defaultValue ? ` ${c.gray(`(${defaultValue})`)}` : ''} ${c.cyan('›')} `)
|
|
77
166
|
rl.close()
|
|
78
167
|
const normalized = answer.trim()
|
|
79
168
|
return normalized || defaultValue
|
|
80
169
|
}
|
|
81
170
|
|
|
171
|
+
// 不支持 raw 模式的终端:退回数字菜单。
|
|
172
|
+
async function chooseOneByNumber(message, choices, safeDefaultIndex) {
|
|
173
|
+
stdout.write(`${questionPrefix(message)}\n`)
|
|
174
|
+
choices.forEach((choice, index) => {
|
|
175
|
+
stdout.write(` ${c.cyan(String(index + 1).padStart(2))}. ${choice.label}${choice.hint ? ` ${c.gray(choice.hint)}` : ''}\n`)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
179
|
+
const answer = (await rl.question(`${c.gray('输入编号')} [${safeDefaultIndex + 1}]: `)).trim()
|
|
180
|
+
rl.close()
|
|
181
|
+
|
|
182
|
+
const numeric = Number(answer)
|
|
183
|
+
if (!answer || !Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
|
|
184
|
+
return choices[safeDefaultIndex].value
|
|
185
|
+
}
|
|
186
|
+
return choices[numeric - 1].value
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesFilter(choice, filter) {
|
|
190
|
+
if (!filter) {
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
const haystack = stripAnsi(`${choice.label} ${choice.hint || ''} ${choice.value}`).toLowerCase()
|
|
194
|
+
const needle = filter.toLowerCase()
|
|
195
|
+
if (haystack.includes(needle)) {
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
// 子序列匹配:输入 flg 也能匹配 feature/login。
|
|
199
|
+
let pointer = 0
|
|
200
|
+
for (const char of haystack) {
|
|
201
|
+
if (char === needle[pointer]) {
|
|
202
|
+
pointer += 1
|
|
203
|
+
if (pointer === needle.length) {
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
|
|
82
211
|
/**
|
|
83
|
-
*
|
|
84
|
-
* 输入为提示文案、选项列表与默认索引,输出为选中的 value。
|
|
212
|
+
* 方向键单选:↑/↓ 移动,回车确认;选项较多时可直接打字过滤;数字键快速选择(不过滤时)。
|
|
85
213
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
214
|
+
async function chooseOneByKeys(message, choices, safeDefaultIndex, options) {
|
|
215
|
+
const pageSize = Math.max(3, Math.min(options.pageSize || 12, (stdout.rows || 24) - 4))
|
|
216
|
+
const filterable = options.filter ?? choices.length > 8
|
|
217
|
+
let filter = ''
|
|
218
|
+
let visible = choices.map((choice, index) => ({ choice, index }))
|
|
219
|
+
let cursor = safeDefaultIndex
|
|
220
|
+
let offset = 0
|
|
221
|
+
let renderedLines = 0
|
|
222
|
+
let selected = null
|
|
223
|
+
|
|
224
|
+
const render = () => {
|
|
225
|
+
if (renderedLines > 0) {
|
|
226
|
+
stdout.write(`\x1b[${renderedLines}F\x1b[J`)
|
|
227
|
+
}
|
|
228
|
+
const hintText = filterable
|
|
229
|
+
? `↑↓ 选择 · 回车确认 · 打字过滤${filter ? ` · 当前: ${c.cyan(filter)}` : ''}`
|
|
230
|
+
: '↑↓ 选择 · 回车确认 · 数字快速选择'
|
|
231
|
+
const lines = [`${questionPrefix(message)} ${c.gray(hintText)}`]
|
|
232
|
+
|
|
233
|
+
if (!visible.length) {
|
|
234
|
+
lines.push(` ${c.gray('没有匹配项,按退格修改')}`)
|
|
235
|
+
} else {
|
|
236
|
+
if (cursor < offset) {
|
|
237
|
+
offset = cursor
|
|
238
|
+
} else if (cursor >= offset + pageSize) {
|
|
239
|
+
offset = cursor - pageSize + 1
|
|
240
|
+
}
|
|
241
|
+
const page = visible.slice(offset, offset + pageSize)
|
|
242
|
+
page.forEach((item, pageIndex) => {
|
|
243
|
+
const active = offset + pageIndex === cursor
|
|
244
|
+
const number = filterable ? '' : c.gray(`${item.index + 1}. `)
|
|
245
|
+
const label = active ? c.cyan(stripAnsi(item.choice.label)) : item.choice.label
|
|
246
|
+
const hint = item.choice.hint ? ` ${c.gray(item.choice.hint)}` : ''
|
|
247
|
+
lines.push(`${active ? c.cyan('❯') : ' '} ${number}${label}${hint}`)
|
|
248
|
+
})
|
|
249
|
+
if (visible.length > pageSize) {
|
|
250
|
+
lines.push(c.gray(` (${cursor + 1}/${visible.length})`))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
stdout.write(`${lines.join('\n')}\n`)
|
|
254
|
+
renderedLines = lines.length
|
|
89
255
|
}
|
|
90
256
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
257
|
+
const applyFilter = () => {
|
|
258
|
+
visible = choices.map((choice, index) => ({ choice, index })).filter((item) => matchesFilter(item.choice, filter))
|
|
259
|
+
cursor = 0
|
|
260
|
+
offset = 0
|
|
94
261
|
}
|
|
95
262
|
|
|
96
|
-
stdout.write(
|
|
97
|
-
|
|
98
|
-
|
|
263
|
+
stdout.write('\x1b[?25l')
|
|
264
|
+
render()
|
|
265
|
+
await readKeys((key) => {
|
|
266
|
+
if (key === '\x1b[A' || (!filterable && key === 'k')) {
|
|
267
|
+
cursor = visible.length ? (cursor - 1 + visible.length) % visible.length : 0
|
|
268
|
+
} else if (key === '\x1b[B' || (!filterable && key === 'j')) {
|
|
269
|
+
cursor = visible.length ? (cursor + 1) % visible.length : 0
|
|
270
|
+
} else if (key === '\r' || key === '\n') {
|
|
271
|
+
if (visible.length) {
|
|
272
|
+
selected = visible[cursor]
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
} else if (key === '\x1b') {
|
|
276
|
+
if (filter) {
|
|
277
|
+
filter = ''
|
|
278
|
+
applyFilter()
|
|
279
|
+
} else {
|
|
280
|
+
stdout.write('\x1b[?25h')
|
|
281
|
+
exitOnCancel()
|
|
282
|
+
}
|
|
283
|
+
} else if (key === '\x7f' || key === '\b') {
|
|
284
|
+
if (filterable && filter) {
|
|
285
|
+
filter = filter.slice(0, -1)
|
|
286
|
+
applyFilter()
|
|
287
|
+
}
|
|
288
|
+
} else if (!filterable && /^[1-9]$/.test(key) && Number(key) <= choices.length) {
|
|
289
|
+
selected = { choice: choices[Number(key) - 1], index: Number(key) - 1 }
|
|
290
|
+
return true
|
|
291
|
+
} else if (filterable && key.length === 1 && key >= ' ') {
|
|
292
|
+
filter += key
|
|
293
|
+
applyFilter()
|
|
294
|
+
}
|
|
295
|
+
render()
|
|
296
|
+
return false
|
|
99
297
|
})
|
|
100
298
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
299
|
+
stdout.write(`\x1b[${renderedLines}F\x1b[J`)
|
|
300
|
+
stdout.write(`${questionPrefix(message)} ${c.cyan(stripAnsi(selected.choice.label))}\n`)
|
|
301
|
+
stdout.write('\x1b[?25h')
|
|
302
|
+
return selected.choice.value
|
|
303
|
+
}
|
|
104
304
|
|
|
105
|
-
|
|
106
|
-
|
|
305
|
+
/**
|
|
306
|
+
* 在给定选项中返回单个值;非交互环境返回默认索引对应值。
|
|
307
|
+
* 输入为提示文案、选项列表({ value, label, hint? })与默认索引,输出为选中的 value。
|
|
308
|
+
* options.filter 强制开启/关闭打字过滤(默认选项超过 8 个时开启)。
|
|
309
|
+
*/
|
|
310
|
+
export async function chooseOne(message, choices, defaultIndex = 0, options = {}) {
|
|
311
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
312
|
+
throw new Error('chooseOne 需要至少一个可选项')
|
|
107
313
|
}
|
|
108
314
|
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
315
|
+
const safeDefaultIndex = Math.min(Math.max(defaultIndex, 0), choices.length - 1)
|
|
316
|
+
if (!isInteractive()) {
|
|
111
317
|
return choices[safeDefaultIndex].value
|
|
112
318
|
}
|
|
113
319
|
|
|
114
|
-
|
|
320
|
+
if (!supportsRawMode()) {
|
|
321
|
+
return chooseOneByNumber(message, choices, safeDefaultIndex)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return chooseOneByKeys(message, choices, safeDefaultIndex, options)
|
|
115
325
|
}
|
|
116
326
|
|
|
117
327
|
/**
|
|
@@ -189,8 +399,8 @@ function renderCheckboxChoices(message, choices, context, cursorIndex, selected,
|
|
|
189
399
|
}
|
|
190
400
|
|
|
191
401
|
const lines = [
|
|
192
|
-
message
|
|
193
|
-
|
|
402
|
+
`${questionPrefix(message)} ${c.gray(`(已选 ${selected.size} 项)`)}`,
|
|
403
|
+
c.gray(` ↑↓ 移动 · 空格 选中/取消 · a 全选/全不选 · 回车 ${context.doneLabel} · q ${context.cancelLabel}`)
|
|
194
404
|
]
|
|
195
405
|
|
|
196
406
|
if (statusMessage) {
|
|
@@ -198,9 +408,11 @@ function renderCheckboxChoices(message, choices, context, cursorIndex, selected,
|
|
|
198
408
|
}
|
|
199
409
|
|
|
200
410
|
choices.forEach((choice, index) => {
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
411
|
+
const active = index === cursorIndex
|
|
412
|
+
const cursor = active ? c.cyan('❯') : ' '
|
|
413
|
+
const checkbox = selected.has(choice.value) ? c.green('◉') : c.gray('◯')
|
|
414
|
+
const label = active ? c.cyan(stripAnsi(choice.label)) : choice.label
|
|
415
|
+
lines.push(`${cursor} ${checkbox} ${label}${choice.hint ? ` ${c.gray(choice.hint)}` : ''}`)
|
|
204
416
|
})
|
|
205
417
|
|
|
206
418
|
stdout.write(`${lines.join('\n')}\n`)
|
|
@@ -229,6 +441,15 @@ function chooseManyByCheckbox(message, choices, context) {
|
|
|
229
441
|
stdin.off('data', onData)
|
|
230
442
|
stdin.setRawMode(wasRaw)
|
|
231
443
|
stdin.pause()
|
|
444
|
+
// 选完后把列表收起成一行摘要。
|
|
445
|
+
if (renderedLines > 0) {
|
|
446
|
+
stdout.write(`\x1b[${renderedLines}F\x1b[J`)
|
|
447
|
+
}
|
|
448
|
+
const labels = value
|
|
449
|
+
? choices.filter((choice) => value.includes(choice.value)).map((choice) => stripAnsi(choice.label))
|
|
450
|
+
: []
|
|
451
|
+
const summary = value == null ? c.gray('已取消') : labels.length ? c.cyan(labels.join(', ')) : c.gray('(未选择)')
|
|
452
|
+
stdout.write(`${questionPrefix(message)} ${summary}\n`)
|
|
232
453
|
resolve(value)
|
|
233
454
|
}
|
|
234
455
|
|
|
@@ -307,6 +528,17 @@ function chooseManyByCheckbox(message, choices, context) {
|
|
|
307
528
|
continue
|
|
308
529
|
}
|
|
309
530
|
|
|
531
|
+
if (key.toLowerCase() === 'a') {
|
|
532
|
+
statusMessage = ''
|
|
533
|
+
if (selected.size === choices.length || selected.size >= context.maxSelections) {
|
|
534
|
+
selected.clear()
|
|
535
|
+
} else {
|
|
536
|
+
choices.slice(0, context.maxSelections).forEach((choice) => selected.add(choice.value))
|
|
537
|
+
}
|
|
538
|
+
render()
|
|
539
|
+
continue
|
|
540
|
+
}
|
|
541
|
+
|
|
310
542
|
if (key.toLowerCase() === 'k') {
|
|
311
543
|
moveCursor(-1)
|
|
312
544
|
continue
|
package/src/core/tools/logger.js
CHANGED
|
@@ -1,53 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
reset: '\x1b[0m',
|
|
3
|
-
gray: '\x1b[90m',
|
|
4
|
-
green: '\x1b[32m',
|
|
5
|
-
yellow: '\x1b[33m',
|
|
6
|
-
red: '\x1b[31m',
|
|
7
|
-
cyan: '\x1b[36m'
|
|
8
|
-
}
|
|
1
|
+
import { c, setColorStream } from './theme.js'
|
|
9
2
|
|
|
10
3
|
let verboseEnabled = process.env.ICODE_LOG_LEVEL === 'verbose'
|
|
4
|
+
// JSON 输出模式下 stdout 只留给结构化结果,普通日志改走 stderr。
|
|
5
|
+
// node --test 的子进程用 stdout 和主进程传递序列化数据,日志混在里面偶尔会导致解析失败,
|
|
6
|
+
// 所以在测试子进程中默认把日志写到 stderr。
|
|
7
|
+
let logsToStderr = Boolean(process.env.NODE_TEST_CONTEXT)
|
|
11
8
|
|
|
12
|
-
function
|
|
13
|
-
return
|
|
9
|
+
function infoStream() {
|
|
10
|
+
return logsToStderr ? process.stderr : process.stdout
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
//
|
|
17
|
-
function print(stream,
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
.
|
|
21
|
-
|
|
13
|
+
// 首行带图标,后续行缩进对齐,多行日志更易读。
|
|
14
|
+
function print(stream, icon, message, colorLine) {
|
|
15
|
+
const lines = String(message ?? '').split('\n')
|
|
16
|
+
const rendered = lines
|
|
17
|
+
.map((line, index) => {
|
|
18
|
+
const content = colorLine ? colorLine(line) : line
|
|
19
|
+
return index === 0 ? `${icon} ${content}` : ` ${content}`
|
|
20
|
+
})
|
|
22
21
|
.join('\n')
|
|
23
22
|
|
|
24
|
-
stream.write(`${
|
|
23
|
+
stream.write(`${rendered}\n`)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export const logger = {
|
|
28
27
|
// 按指定颜色渲染短文本,输入颜色名和文本,输出可直接拼进日志的 ANSI 字符串。
|
|
29
28
|
color(color, message) {
|
|
30
|
-
|
|
29
|
+
const painter = c[color]
|
|
30
|
+
return painter ? painter(String(message ?? '')) : String(message ?? '')
|
|
31
31
|
},
|
|
32
32
|
|
|
33
33
|
setVerbose(enabled) {
|
|
34
34
|
verboseEnabled = Boolean(enabled)
|
|
35
35
|
},
|
|
36
36
|
|
|
37
|
+
setLogsToStderr(enabled) {
|
|
38
|
+
logsToStderr = Boolean(enabled)
|
|
39
|
+
setColorStream(logsToStderr ? process.stderr : null)
|
|
40
|
+
},
|
|
41
|
+
|
|
37
42
|
info(message) {
|
|
38
|
-
print(
|
|
43
|
+
print(infoStream(), c.cyan('›'), `${message}`)
|
|
39
44
|
},
|
|
40
45
|
|
|
41
46
|
success(message) {
|
|
42
|
-
print(
|
|
47
|
+
print(infoStream(), c.green('✔'), `${message}`)
|
|
43
48
|
},
|
|
44
49
|
|
|
45
50
|
warn(message) {
|
|
46
|
-
print(process.stderr,
|
|
51
|
+
print(process.stderr, c.yellow('▲'), `${message}`)
|
|
47
52
|
},
|
|
48
53
|
|
|
49
54
|
error(message) {
|
|
50
|
-
print(process.stderr,
|
|
55
|
+
print(process.stderr, c.red('✖'), `${message}`, c.red)
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// 次要信息(提示、下一步建议),灰色显示。
|
|
59
|
+
hint(message) {
|
|
60
|
+
print(infoStream(), c.gray('·'), `${message}`, c.gray)
|
|
51
61
|
},
|
|
52
62
|
|
|
53
63
|
debug(message) {
|
|
@@ -55,6 +65,6 @@ export const logger = {
|
|
|
55
65
|
return
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
print(
|
|
68
|
+
print(infoStream(), c.gray('[debug]'), `${message}`, c.gray)
|
|
59
69
|
}
|
|
60
70
|
}
|