@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.
Files changed (75) hide show
  1. package/README.md +287 -24
  2. package/bin/icode.js +10 -0
  3. package/package.json +3 -3
  4. package/src/cli.js +175 -8
  5. package/src/commands/checkout.js +73 -6
  6. package/src/commands/clean.js +47 -7
  7. package/src/commands/config-ux.js +292 -0
  8. package/src/commands/config.js +199 -48
  9. package/src/commands/continue.js +104 -0
  10. package/src/commands/doctor.js +15 -29
  11. package/src/commands/help.js +54 -34
  12. package/src/commands/history.js +147 -0
  13. package/src/commands/info.js +39 -4
  14. package/src/commands/log.js +142 -0
  15. package/src/commands/migrate.js +19 -12
  16. package/src/commands/open.js +138 -0
  17. package/src/commands/pr.js +219 -0
  18. package/src/commands/push.js +62 -22
  19. package/src/commands/ready.js +18 -48
  20. package/src/commands/release.js +112 -0
  21. package/src/commands/rescue.js +164 -0
  22. package/src/commands/save.js +52 -11
  23. package/src/commands/stash.js +210 -0
  24. package/src/commands/sw.js +89 -0
  25. package/src/commands/sync.js +19 -2
  26. package/src/commands/tag.js +40 -1
  27. package/src/commands/undo.js +9 -3
  28. package/src/core/ai/diff-range.js +1 -1
  29. package/src/core/cli/completion.js +237 -1
  30. package/src/core/cli/output.js +46 -0
  31. package/src/core/cli/render.js +121 -0
  32. package/src/core/commands/shell.js +9 -1
  33. package/src/core/config/repo-config.js +93 -0
  34. package/src/core/config/store.js +60 -10
  35. package/src/core/git/autostash.js +46 -0
  36. package/src/core/git/backup.js +170 -0
  37. package/src/core/git/conflicts.js +58 -0
  38. package/src/core/git/context.js +72 -5
  39. package/src/core/git/file-guard.js +193 -0
  40. package/src/core/git/operation-state.js +63 -0
  41. package/src/core/git/operations.js +32 -0
  42. package/src/core/git/semver.js +194 -0
  43. package/src/core/git/service.js +454 -24
  44. package/src/core/platform/http.js +53 -0
  45. package/src/core/platform/index.js +165 -0
  46. package/src/core/platform/providers.js +454 -0
  47. package/src/core/platform/remote-url.js +92 -0
  48. package/src/core/remote-merge/client.js +33 -3
  49. package/src/core/tools/browser.js +28 -0
  50. package/src/core/tools/commit-prompt.js +137 -0
  51. package/src/core/tools/interactive.js +272 -40
  52. package/src/core/tools/logger.js +33 -23
  53. package/src/core/tools/theme.js +228 -0
  54. package/src/workflows/ai-commit-workflow.js +52 -20
  55. package/src/workflows/checkout-workflow.js +24 -10
  56. package/src/workflows/clean-workflow.js +135 -83
  57. package/src/workflows/continue-workflow.js +233 -0
  58. package/src/workflows/doctor-workflow.js +10 -7
  59. package/src/workflows/history-workflow.js +211 -0
  60. package/src/workflows/migrate-workflow.js +39 -14
  61. package/src/workflows/pr-workflow.js +252 -0
  62. package/src/workflows/push/commit-step.js +123 -0
  63. package/src/workflows/push/local-merge-step.js +213 -0
  64. package/src/workflows/push/plan.js +63 -0
  65. package/src/workflows/push/remote-merge-step.js +224 -0
  66. package/src/workflows/push-workflow.js +185 -492
  67. package/src/workflows/ready-workflow.js +14 -10
  68. package/src/workflows/release-workflow.js +189 -0
  69. package/src/workflows/rescue-workflow.js +137 -0
  70. package/src/workflows/save-workflow.js +106 -41
  71. package/src/workflows/switch-workflow.js +171 -0
  72. package/src/workflows/sync-workflow.js +143 -44
  73. package/src/workflows/tag-workflow.js +103 -17
  74. package/src/workflows/undo-workflow.js +113 -34
  75. 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 rl = createInterface({ input: stdin, output: stdout })
47
- const suffix = defaultValue ? '[Y/n]' : '[y/N]'
48
- const answer = (await rl.question(`${message} ${suffix} `)).trim().toLowerCase()
49
- rl.close()
50
-
51
- if (!answer) {
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
- if (['y', 'yes', '1', 'true', 'ok', 'sure', '是'].includes(answer)) {
56
- return true
57
- }
58
-
59
- if (['n', 'no', '0', 'false', '否'].includes(answer)) {
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 defaultValue
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
- const answer = await rl.question(`${message}${defaultValue ? ` (${defaultValue})` : ''}: `)
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
- export async function chooseOne(message, choices, defaultIndex = 0) {
87
- if (!Array.isArray(choices) || choices.length === 0) {
88
- throw new Error('chooseOne 需要至少一个可选项')
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 safeDefaultIndex = Math.min(Math.max(defaultIndex, 0), choices.length - 1)
92
- if (!isInteractive()) {
93
- return choices[safeDefaultIndex].value
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(`${message}\n`)
97
- choices.forEach((choice, index) => {
98
- stdout.write(` ${index + 1}. ${choice.label}\n`)
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
- const rl = createInterface({ input: stdin, output: stdout })
102
- const answer = (await rl.question(`请选择 [${safeDefaultIndex + 1}]: `)).trim()
103
- rl.close()
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
- if (!answer) {
106
- return choices[safeDefaultIndex].value
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 numeric = Number(answer)
110
- if (!Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
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
- return choices[numeric - 1].value
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
- `↑/↓ 移动,Space 选中/取消,Enter ${context.doneLabel},q ${context.cancelLabel}(已选 ${selected.size} 项)`
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 cursor = index === cursorIndex ? '>' : ' '
202
- const checkbox = selected.has(choice.value) ? '[x]' : '[ ]'
203
- lines.push(`${cursor} ${checkbox} ${choice.label}`)
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
@@ -1,53 +1,63 @@
1
- const COLORS = {
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 colorize(color, message) {
13
- return `${COLORS[color] || ''}${message}${COLORS.reset}`
9
+ function infoStream() {
10
+ return logsToStderr ? process.stderr : process.stdout
14
11
  }
15
12
 
16
- // Prefix every rendered line so multiline logs keep the same visible marker.
17
- function print(stream, level, message) {
18
- const normalizedMessage = String(message ?? '')
19
- const formattedMessage = normalizedMessage
20
- .split('\n')
21
- .map((line) => `${level}${line}`)
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(`${formattedMessage}\n`)
23
+ stream.write(`${rendered}\n`)
25
24
  }
26
25
 
27
26
  export const logger = {
28
27
  // 按指定颜色渲染短文本,输入颜色名和文本,输出可直接拼进日志的 ANSI 字符串。
29
28
  color(color, message) {
30
- return colorize(color, String(message ?? ''))
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(process.stdout, colorize('cyan', '[icode] '), `${message}`)
43
+ print(infoStream(), c.cyan('›'), `${message}`)
39
44
  },
40
45
 
41
46
  success(message) {
42
- print(process.stdout, colorize('green', '[icode] '), `${message}`)
47
+ print(infoStream(), c.green('✔'), `${message}`)
43
48
  },
44
49
 
45
50
  warn(message) {
46
- print(process.stderr, colorize('yellow', '[icode] '), `${message}`)
51
+ print(process.stderr, c.yellow('▲'), `${message}`)
47
52
  },
48
53
 
49
54
  error(message) {
50
- print(process.stderr, colorize('red', '[icode] '), `${message}`)
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(process.stdout, colorize('gray', '[icode:debug] '), `${message}`)
68
+ print(infoStream(), c.gray('[debug]'), `${message}`, c.gray)
59
69
  }
60
70
  }