@kaynooo/cli-kit 0.0.0-stage → 0.7.3
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 +59 -2
- package/package.json +36 -4
- package/src/args.ts +189 -0
- package/src/chart.ts +61 -0
- package/src/cli.ts +234 -0
- package/src/config.ts +59 -0
- package/src/format.ts +163 -0
- package/src/index.ts +8 -0
- package/src/process.ts +120 -0
- package/src/progress.ts +211 -0
- package/src/prompt.ts +331 -0
- package/src/shell.ts +373 -0
- package/src/style.ts +73 -0
- package/src/test.ts +42 -0
- package/src/text.ts +21 -0
package/src/style.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { createColors } from '@kaynooo/utils/tools/colors'
|
|
3
|
+
|
|
4
|
+
export type Colors = ReturnType<typeof createColors>
|
|
5
|
+
|
|
6
|
+
export type Mark = 'ok' | 'warn' | 'problem' | 'note'
|
|
7
|
+
|
|
8
|
+
// The symbol each mark draws on a terminal, and the word it prints as anywhere else, so a log stays greppable.
|
|
9
|
+
const MARKS: Record<Mark, { symbol: string, word: string, paint: (colors: Colors) => (text: string) => string }> = {
|
|
10
|
+
ok: { symbol: '✓', word: 'ok:', paint: colors => colors.green },
|
|
11
|
+
warn: { symbol: '!', word: 'warning:', paint: colors => colors.yellow },
|
|
12
|
+
problem: { symbol: '✗', word: 'error:', paint: colors => colors.red },
|
|
13
|
+
note: { symbol: '·', word: 'note:', paint: colors => colors.dim },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A terminal that draws in place: not a log or a pipe, and not asked for plain output by `NO_COLOR` or a dumb `TERM`.
|
|
17
|
+
export function takesControlCodes(stream: { isTTY?: boolean }, env: Record<string, string | undefined> = process.env): boolean {
|
|
18
|
+
return stream.isTTY === true && env.NO_COLOR === undefined && env.NODE_DISABLE_COLORS === undefined && env.TERM !== 'dumb'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Colours that write escapes only to a stream taking control codes.
|
|
22
|
+
export function paint(stream: { isTTY?: boolean }, env: Record<string, string | undefined> = process.env): Colors {
|
|
23
|
+
return createColors({ stream: { isTTY: takesControlCodes(stream, env) }, env })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `✓ text` on a terminal, `ok: text` elsewhere; `detail` follows dimmed.
|
|
27
|
+
export function marked(mark: Mark, text: string, stream: { isTTY?: boolean }, detail?: string, env: Record<string, string | undefined> = process.env): string {
|
|
28
|
+
const colors = paint(stream, env)
|
|
29
|
+
const { symbol, word, paint: tint } = MARKS[mark]
|
|
30
|
+
const head = takesControlCodes(stream, env) ? tint(colors)(symbol) : word
|
|
31
|
+
|
|
32
|
+
return `${head} ${text}${detail === undefined ? '' : ` ${colors.dim(detail)}`}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ok(text: string, detail?: string): void {
|
|
36
|
+
console.log(marked('ok', text, process.stdout, detail))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function note(text: string, detail?: string): void {
|
|
40
|
+
console.log(marked('note', text, process.stdout, detail))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function warn(text: string, detail?: string): void {
|
|
44
|
+
console.error(marked('warn', text, process.stderr, detail))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function problem(text: string, detail?: string): void {
|
|
48
|
+
console.error(marked('problem', text, process.stderr, detail))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type Role = 'accent' | 'id' | 'path' | 'label' | 'value'
|
|
52
|
+
|
|
53
|
+
// The tint each kind of data is drawn in, so every CLI colours the same things the same way.
|
|
54
|
+
const ROLES: Record<Role, (colors: Colors) => (text: string) => string> = {
|
|
55
|
+
accent: colors => colors.cyan,
|
|
56
|
+
id: colors => colors.yellow,
|
|
57
|
+
path: colors => colors.blue,
|
|
58
|
+
label: colors => colors.dim,
|
|
59
|
+
value: colors => colors.bold,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// `role('id', process.stdout)('#49')`: plain text on a stream that takes no control codes.
|
|
63
|
+
export function role(name: Role, stream: { isTTY?: boolean }, env: Record<string, string | undefined> = process.env): (text: string) => string {
|
|
64
|
+
return ROLES[name](paint(stream, env))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A clickable OSC 8 link on a terminal; elsewhere `text (url)`, or the text alone when it is the url.
|
|
68
|
+
export function link(text: string, url: string, stream: { isTTY?: boolean }, env: Record<string, string | undefined> = process.env): string {
|
|
69
|
+
if (takesControlCodes(stream, env))
|
|
70
|
+
return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`
|
|
71
|
+
|
|
72
|
+
return text === url ? text : `${text} (${url})`
|
|
73
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { format } from 'node:util'
|
|
3
|
+
|
|
4
|
+
export interface Captured<T> {
|
|
5
|
+
result: T
|
|
6
|
+
// Everything printed to stdout (console.log, console.info and process.stdout.write).
|
|
7
|
+
out: string
|
|
8
|
+
// Everything printed to stderr (console.error, console.warn and process.stderr.write).
|
|
9
|
+
err: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Runs a command in-process and returns what it printed, so a test needs no spy on console. Output is restored even when the command throws.
|
|
13
|
+
export async function capture<T>(run: () => Promise<T> | T): Promise<Captured<T>> {
|
|
14
|
+
const written = { out: '', err: '' }
|
|
15
|
+
const saved = { log: console.log, info: console.info, error: console.error, warn: console.warn, stdout: process.stdout.write.bind(process.stdout), stderr: process.stderr.write.bind(process.stderr) }
|
|
16
|
+
const line = (name: 'out' | 'err') => (...parts: unknown[]) => {
|
|
17
|
+
written[name] += `${format(...parts)}\n`
|
|
18
|
+
}
|
|
19
|
+
const chunk = (name: 'out' | 'err') => (data: string | Uint8Array) => {
|
|
20
|
+
written[name] += typeof data === 'string' ? data : Buffer.from(data).toString()
|
|
21
|
+
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log = line('out')
|
|
26
|
+
console.info = line('out')
|
|
27
|
+
console.error = line('err')
|
|
28
|
+
console.warn = line('err')
|
|
29
|
+
process.stdout.write = chunk('out')
|
|
30
|
+
process.stderr.write = chunk('err')
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const result = await run()
|
|
34
|
+
|
|
35
|
+
return { result, ...written }
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
Object.assign(console, { log: saved.log, info: saved.info, error: saved.error, warn: saved.warn })
|
|
39
|
+
process.stdout.write = saved.stdout
|
|
40
|
+
process.stderr.write = saved.stderr
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[\\d;?]*[a-z]`, 'gi')
|
|
2
|
+
|
|
3
|
+
export function stripAnsi(text: string): string {
|
|
4
|
+
return text.replace(ANSI, '')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Edit distance between two names, for pointing a mistyped name at the one meant.
|
|
8
|
+
export function distance(a: string, b: string): number {
|
|
9
|
+
let previous = Array.from({ length: b.length + 1 }, (_, index) => index)
|
|
10
|
+
|
|
11
|
+
for (const [i, left] of [...a].entries()) {
|
|
12
|
+
const current = [i + 1]
|
|
13
|
+
|
|
14
|
+
for (const [j, right] of [...b].entries())
|
|
15
|
+
current.push(Math.min((previous[j + 1] ?? 0) + 1, (current[j] ?? 0) + 1, (previous[j] ?? 0) + (left === right ? 0 : 1)))
|
|
16
|
+
|
|
17
|
+
previous = current
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return previous[b.length] ?? 0
|
|
21
|
+
}
|