@kaynooo/cli-kit 0.13.0 → 0.15.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 +6 -5
- package/package.json +4 -4
- package/src/args.ts +15 -7
- package/src/logs.ts +5 -2
- package/src/progress.ts +37 -1
- package/src/test.ts +22 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ What every @kaynooo CLI shares, so each one parses, helps and fails the same way
|
|
|
6
6
|
bun add @kaynooo/cli-kit
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
It ships its TypeScript source and runs on Bun only (>= 1.4.3). The
|
|
9
|
+
It ships its TypeScript source and runs on Bun only (>= 1.4.3). The @kaynooo/cli-* packages take it as a `workspace:*` dev dependency and bundle it at build; a CLI elsewhere takes it from npmjs as a dependency (`bun add --exact @kaynooo/cli-kit`).
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
import { parse, runCli } from '@kaynooo/cli-kit'
|
|
@@ -44,13 +44,14 @@ On a terminal, help has bold `Usage:`, `Commands:` and `Options:` headers, comma
|
|
|
44
44
|
- `<bin> __complete <words>... <word>` is answered by `runCli` and `runMain` for shell completion: the subcommands with their summaries, a leaf command's options (read by running it until its `parse` call throws them, so every command parses its arguments first), or `__files__` where a path fits.
|
|
45
45
|
- `parse(args, usage, options, { rest: true })` stops at the first `--` and returns what follows untouched as `rest` (`out -n 200 -- <cmd>`), `undefined` without a `--`; `{ allowNegative: true }` takes `--no-<flag>` as `false` for a boolean flag.
|
|
46
46
|
- `readInput(path, { noStdin })` reads a file, or stdin when the path is `-`; a missing file or a stdin left on the terminal ends the command with a message.
|
|
47
|
+
- `isStdinPiped()` is true when stdin is a pipe or a file, false on a terminal or `/dev/null`, so a command can read what is piped in without waiting on a terminal.
|
|
47
48
|
- `runCommand(argv, { cwd, env, stdin, timeoutMs, signal, maxBytes, onChunk, onStart })` runs a command and returns `{ code, text, output, chunks, timedOut, truncated }`, the output of both streams (in arrival order, tagged by stream in `chunks`); it throws when the command cannot start. With `timeoutMs`, `signal`, `onChunk` or `onStart` the child leads its own process group, ended whole (SIGTERM, SIGKILL after 2 s) when the timeout passes, the signal aborts or `onChunk` throws, the pipes still read to the end: `timedOut` is then true, for the caller to exit 2. `maxBytes` caps what `chunks`, `output` and `text` keep (`truncated`), `onChunk` sees every chunk, and `onStart` hands over `{ kill }` for the whole group, to pass to `holdSignals().forward`. `exitCodeOf(code, signal)` is the exit code a shell reports.
|
|
48
49
|
- `holdSignals(cleanup?)` takes SIGINT, SIGTERM and SIGHUP over until `release()`: a signal goes to the child given to `forward(child)`, so the CLI exits with the child's code; without one, `cleanup(signal)` runs and the signal is raised again, ending the process as it would have; without either, it is only kept in `signal`, for the caller to wind down (`base` holds them while its worktree exists).
|
|
49
|
-
- `privateLogDirectory(name)` returns a folder for log files only this user reads (`$XDG_RUNTIME_DIR` or `~/.cache/
|
|
50
|
+
- `privateLogDirectory(name)` returns a folder for log files only this user reads (`$XDG_RUNTIME_DIR` or `~/.cache/kaynooo-cli`, mode 0700, verified with `lstat` when it exists, else a fresh temp folder), `createLog(path)` opens a 0600 file that must not exist (returning its descriptor), and `sweepLogs(dir, maxAgeMs?)` removes what is older than a day, subfolders included.
|
|
50
51
|
- `openUrl(target, env?)` opens a URL, or a file path made absolute, with the command `BROWSER` names or the platform's opener (`xdg-open`, `open`, `start`), without waiting; it returns `false` when that command cannot start.
|
|
51
52
|
- Every command's `parse`, and `runCli` before the command name (`ft --plain runs`), takes `--color auto|always|never` and `--plain` (`--color never`), unless the command has its own option of that name. `always` sets `FORCE_COLOR=1` and `never` sets `NO_COLOR=1`, each dropping the other, so the kit's colours, marks and redrawn lines follow, and so do the processes the command starts; `always` colours a pipe but never redraws in one. `applyOutput(color, plain, env?)` is what they run.
|
|
52
53
|
- `--help` lists the commands under `Commands:`, then a heading per `group` a command names (`{ summary, group: 'Admin', run }`), then the level's `Options:` and the `Examples:` the CLI or a group's `examples` give (`['ft ci', ['ft ci HEAD~1', 'the commit before']]`, a note shown dimmed after `#`); a command's own examples go to `parse(args, usage, options, { examples })`, shown under its options.
|
|
53
|
-
- `replaces` on the CLI or in `parse`'s settings (`'sed -n N,Mp, grep -n chains'`) adds an `Instead of:` line under the usage. With `KIT_HELP=json` in the environment, `--help` prints `{synopsis, summary, replaces?, examples}` instead, the usage's first line split at its first `: `, which is how `
|
|
54
|
+
- `replaces` on the CLI or in `parse`'s settings (`'sed -n N,Mp, grep -n chains'`) adds an `Instead of:` line under the usage. With `KIT_HELP=json` in the environment, `--help` prints `{synopsis, summary, replaces?, examples}` instead, the usage's first line split at its first `: `, which is how `cli list` reads every bin.
|
|
54
55
|
- `runHint(command, effect?)` words the hint of a refusal that a command resolves, ``run `ft take 12 --take-stale` to take it over``, so every CLI writes it alike: `fail(message, 1, runHint('ft take 12 --take-stale', 'take it over'))`. `printHint(hint)` prints the `hint:` line under a refusal a command prints itself (one issue of several refused).
|
|
55
56
|
- `limitOptions(fallback)` is the `--limit <n>` and `--full` flags of a listing, `limitOf(values, fallback)` the row count they ask for (Infinity with `--full`; with `json: true` in the values, Infinity unless `--limit` is given, so JSON is never cut on its own), and `capped(rows, limit)` the first rows plus the footer `N more, rerun with --full` when some are left out.
|
|
56
57
|
- `jsonOption(holds?)` is the `--json` flag, worded `print JSON` or `print JSON: <holds>` alike in every CLI, and `printJson(value)` prints the value indented on stdout, bare, with no envelope. Under `--json` runMain and runCli print a thrown error as `{"error", "hint"?, "cause"?}` on stderr instead of the `error:` line, its exit code unchanged; `printJsonError(message, hint?, cause?)` prints one for a command reporting a failure itself.
|
|
@@ -58,7 +59,7 @@ On a terminal, help has bold `Usage:`, `Commands:` and `Options:` headers, comma
|
|
|
58
59
|
- `table(rows, { cellWidth, header })` also flattens each cell's line breaks and cuts it to `cellWidth`, and with `header` prints the first row bold on a terminal; and `formatDuration(ms)` formats milliseconds like `formatSeconds` but with one decimal under a minute (`0.3s`).
|
|
59
60
|
- `ok(text, detail?)`, `note`, `warn` and `problem` print a status line marked with a green ✓, a dimmed ·, a yellow ! or a red ✗ (`ok` and `note` on stdout, the others on stderr), the detail dimmed after the text; plain, the marks read `ok:`, `note:`, `warning:` and `error:`. `marked(mark, text, stream, detail?)` returns such a line, `paint(stream)` the colours for a stream and `takesControlCodes(stream)` whether it draws in place.
|
|
60
61
|
- `isSecretName(name)` is true for the name of a variable, column or JSON key that holds a secret (`API_TOKEN`, `DB_PASS`, `SMTP_PWD`, `clientSecret`, `session_id`, `ssn`), by whole words so `compass` and `author` stay plain; `ev`, `redact` and `db` share it.
|
|
61
|
-
- `capture(fn)` from `@kaynooo/cli-kit/test` runs a command in-process and returns `{ result, out, err }`, what it printed through `console` and `process.stdout`/`stderr`, so a test needs no spy.
|
|
62
|
+
- `capture(fn)` from `@kaynooo/cli-kit/test` runs a command in-process and returns `{ result, out, err }`, what it printed through `console` and `process.stdout`/`stderr`, so a test needs no spy. `tempDirs()`, called at the top of a test file, answers a `temp(prefix?)` making fresh folders under the system temp folder, all removed once the file's tests are done.
|
|
62
63
|
- `findUp(names, from?)` is the nearest of `names` in the current folder (or `from`) or one above it; `loadConfig(names, { from?, path? })` imports the nearest config (`.ts`, `.js` or `.json`), or the file `path` names, and returns `{ path, dir, config }` with its default export for the caller to check, `undefined` when there is none. A named file missing, one failing to import or one without a default export ends the command with exit 1.
|
|
63
64
|
- `@kaynooo/cli-kit/prompt` asks on the terminal, on stderr:
|
|
64
65
|
- `select(question, choices, { initial })` picks one `{ value, label, hint }`: typing filters the choices (every word typed in the label or hint, case ignored, Backspace editing), the arrows move and Enter picks.
|
|
@@ -69,7 +70,7 @@ On a terminal, help has bold `Usage:`, `Commands:` and `Options:` headers, comma
|
|
|
69
70
|
- `@kaynooo/cli-kit/progress` draws `progressLine(stream?, env?)` on stderr, one line redrawn in place while a task runs (`⠹ prerendering ████████░░░░░░░░░░░░ 8/20 routes 4.2s`, the bar cyan on a dimmed track), fed `update({ step, done?, total?, unit? })` and ended with `stop()`, `done(text)` leaving `✓ <text> <time>` or `failed(text)` leaving `✗ <text>`. It draws only where `takesControlCodes`; elsewhere `update` and `stop` do nothing and `done` and `failed` print their plain line. Anything printed meanwhile clears it first, and an interrupt clears it before the process ends.
|
|
70
71
|
- `task(label, run)` from `/progress` runs one step under a spinner, `run` given an `update({ done?, total?, unit? })` for a bar, and ends on `✓ <label> <time>`, or `✗ <label>` and the error thrown again.
|
|
71
72
|
- `taskList(stream?, env?)` from `/progress` stacks a progress line per task running side by side: `add(label)` returns `{ update, done(text?), failed(text?) }`, and a task ending leaves its `✓` or `✗` line above those still running.
|
|
72
|
-
- `liveRegion(render, stream?, env?)` from `/progress` is the block both draw with: the lines `render(spinner)` returns, redrawn every frame (`redraw()` draws at once), each cut to the terminal's width and the block to its height, `print(text)` printing above it and `stop()` clearing it. Like the progress line it draws only where `takesControlCodes
|
|
73
|
+
- `liveRegion(render, stream?, env?)` from `/progress` is the block both draw with: the lines `render(spinner)` returns, redrawn every frame (`redraw()` draws at once), each cut to the terminal's width and the block to its height, `print(text)` printing above it and `stop()` clearing it. Like the progress line it draws only where `takesControlCodes`; keys typed on the terminal meanwhile are swallowed, Ctrl+C aside, so an Enter cannot push it down and leave a line behind. `ft ci` and `ft ci` and `waitfor` show what they wait on with it.
|
|
73
74
|
- `badge(word, stream)` marks a status word on a terminal, the same five ways everywhere: `✓ success` green, `✗ failure` red, `● running` cyan, `○ waiting` yellow, `- skipped` dimmed; the word alone elsewhere. `statusOf(word)` reads the words Forgejo and GitLab report (`success`, `failed`, `pending`, `canceled`...) as `passed`, `failed`, `running`, `queued` or `skipped`.
|
|
74
75
|
- `keyValue(pairs)` aligns `label value` lines, `sparkline(values)` draws `▁▃▇`, `bar(ratio)` and `barChart(rows)` draw cyan bars on a dimmed track, and `tree(roots)` hangs `{ label, children? }` nodes on dimmed `├─`, `└─` branches.
|
|
75
76
|
- `@kaynooo/cli-kit/shell` reads shell command lines the way a hook vetting them needs: `splitCommand` cuts one into pieces at the separators outside quotes and heredocs, `parseCommand` names a piece's program past `sudo`/`env` wrappers and assignments, `tokenize`, `words`, `substitutions` and `replacePieces` take it apart and put it back.
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/cli-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"description": "What every @kaynooo CLI shares: argument parsing with --help, a command dispatcher with typo hints and shell completion, and output formatting.",
|
|
6
6
|
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://git.kaynooo.fr/kaynooo/
|
|
8
|
+
"homepage": "https://git.kaynooo.fr/kaynooo/cli/src/branch/main/packages/kit#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://git.kaynooo.fr/kaynooo/
|
|
11
|
+
"url": "git+https://git.kaynooo.fr/kaynooo/cli.git",
|
|
12
12
|
"directory": "packages/kit"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://git.kaynooo.fr/kaynooo/
|
|
14
|
+
"bugs": "https://git.kaynooo.fr/kaynooo/cli/issues",
|
|
15
15
|
"keywords": [
|
|
16
16
|
"args",
|
|
17
17
|
"bun",
|
package/src/args.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ParseArgsOptionDescriptor } from 'node:util'
|
|
2
|
+
import { fstatSync } from 'node:fs'
|
|
2
3
|
import process from 'node:process'
|
|
3
4
|
import { parseArgs } from 'node:util'
|
|
4
5
|
import { upTo } from './format'
|
|
@@ -68,7 +69,7 @@ export function replacesRows(replaces: string | undefined, colors = paint(proces
|
|
|
68
69
|
return replaces === undefined ? [] : ['', `${colors.bold('Instead of:')} ${replaces}`]
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
// What a bin's --help prints under KIT_HELP=json, for `
|
|
72
|
+
// What a bin's --help prints under KIT_HELP=json, for `cli list` to read without parsing the text.
|
|
72
73
|
export interface Description {
|
|
73
74
|
synopsis: string
|
|
74
75
|
summary: string
|
|
@@ -131,7 +132,7 @@ export interface ParseSettings<Names extends readonly string[] = string[]> {
|
|
|
131
132
|
positionals?: Names
|
|
132
133
|
// Command lines shown under `Examples:` in the help, each with an optional dimmed note: `['ft ci HEAD~1', 'wait for the commit before the last']`.
|
|
133
134
|
examples?: Example[]
|
|
134
|
-
// What the command is reached for instead of, shown as `Instead of:` in the help and listed by `
|
|
135
|
+
// What the command is reached for instead of, shown as `Instead of:` in the help and listed by `cli list`: 'sed -n N,Mp, grep -n chains and whole-file reads'.
|
|
135
136
|
replaces?: string
|
|
136
137
|
}
|
|
137
138
|
|
|
@@ -291,11 +292,11 @@ export function wholeNumber(value: string | undefined, name: string, fallback: n
|
|
|
291
292
|
return number
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
const DURATION = /^(\d+(?:\.\d+)?)(ms|[
|
|
295
|
-
const UNIT_MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 } as const
|
|
295
|
+
const DURATION = /^(\d+(?:\.\d+)?)(ms|[smhd])?$/
|
|
296
|
+
const UNIT_MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 } as const
|
|
296
297
|
|
|
297
|
-
// A duration such as `500ms`, `90s`, `5m` or `
|
|
298
|
-
export function duration(value: string | undefined, name: string, fallback: number, unit: 's' | 'm' = 's'): number {
|
|
298
|
+
// A duration such as `500ms`, `90s`, `5m`, `1h` or `7d` in milliseconds, a bare number counting in `unit`.
|
|
299
|
+
export function duration(value: string | undefined, name: string, fallback: number, unit: 's' | 'm' | 'h' = 's'): number {
|
|
299
300
|
if (value === undefined)
|
|
300
301
|
return fallback
|
|
301
302
|
|
|
@@ -303,11 +304,18 @@ export function duration(value: string | undefined, name: string, fallback: numb
|
|
|
303
304
|
const ms = match === null ? 0 : Number(match[1]) * UNIT_MS[(match[2] ?? unit) as keyof typeof UNIT_MS]
|
|
304
305
|
|
|
305
306
|
if (ms < 1)
|
|
306
|
-
badUsage(`--${name} takes a duration such as 30s, 5m or
|
|
307
|
+
badUsage(`--${name} takes a duration such as 30s, 5m, 1h or 7d, not ${value}`)
|
|
307
308
|
|
|
308
309
|
return ms
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
// Whether stdin is a pipe or a file to read, rather than a terminal or /dev/null that would leave a command waiting or reading nothing.
|
|
313
|
+
export function isStdinPiped(): boolean {
|
|
314
|
+
const stat = fstatSync(0)
|
|
315
|
+
|
|
316
|
+
return stat.isFIFO() || stat.isFile()
|
|
317
|
+
}
|
|
318
|
+
|
|
311
319
|
export interface InputSettings {
|
|
312
320
|
// What to say when stdin is a terminal, as nothing is piped.
|
|
313
321
|
noStdin?: string
|
package/src/logs.ts
CHANGED
|
@@ -6,6 +6,9 @@ import process from 'node:process'
|
|
|
6
6
|
|
|
7
7
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
8
8
|
|
|
9
|
+
// The folder the @kaynooo/cli-* bins keep their logs and state in, under the runtime, cache or state directory.
|
|
10
|
+
export const CLI_FOLDER = 'kaynooo-cli'
|
|
11
|
+
|
|
9
12
|
// Whether the folder is a real directory (not a link) that only this user can use.
|
|
10
13
|
function isPrivateDirectory(path: string): boolean {
|
|
11
14
|
try {
|
|
@@ -18,10 +21,10 @@ function isPrivateDirectory(path: string): boolean {
|
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
// A folder for log files that only this user can read: `<name>` under $XDG_RUNTIME_DIR, else ~/.cache/
|
|
24
|
+
// A folder for log files that only this user can read: `<name>` under $XDG_RUNTIME_DIR, else ~/.cache/kaynooo-cli. When one already exists it must be a private directory of this user, otherwise a fresh private folder in the temp directory is used instead of trusting it.
|
|
22
25
|
export async function privateLogDirectory(name: string, env: Record<string, string | undefined> = process.env): Promise<string> {
|
|
23
26
|
const runtime = env.XDG_RUNTIME_DIR
|
|
24
|
-
const base = runtime === undefined || runtime === '' ? join(homedir(), '.cache',
|
|
27
|
+
const base = runtime === undefined || runtime === '' ? join(homedir(), '.cache', CLI_FOLDER) : join(runtime, CLI_FOLDER)
|
|
25
28
|
const path = join(base, name)
|
|
26
29
|
|
|
27
30
|
try {
|
package/src/progress.ts
CHANGED
|
@@ -62,6 +62,8 @@ const ESC = String.fromCharCode(27)
|
|
|
62
62
|
// A colour or cursor code, or an OSC 8 link's opening or closing, read where `lastIndex` points.
|
|
63
63
|
const ESCAPE_CODE = new RegExp(`${ESC}(?:\\[[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]|\\][^\\u0007${ESC}]*(?:\\u0007|${ESC}\\\\))`, 'y')
|
|
64
64
|
const RESET_STYLE = `${ESC}[0m`
|
|
65
|
+
// What Ctrl+C sends once raw mode stops the terminal turning it into SIGINT.
|
|
66
|
+
const CTRL_C = 3
|
|
65
67
|
const CLOSE_LINK = `${ESC}]8;;${ESC}\\`
|
|
66
68
|
// What the console prints with, to the terminal the line is drawn on or the other.
|
|
67
69
|
const CONSOLE_METHODS = ['log', 'info', 'warn', 'error', 'debug', 'dir', 'dirxml', 'table', 'trace', 'group', 'groupCollapsed', 'count', 'timeEnd', 'timeLog', 'assert'] as const
|
|
@@ -102,7 +104,11 @@ export function liveRegion(render: (spinner: string) => string[], stream: Progre
|
|
|
102
104
|
}
|
|
103
105
|
const timer = setInterval(draw, FRAME_MS)
|
|
104
106
|
const restore = interceptOutput(clear)
|
|
105
|
-
const
|
|
107
|
+
const release = swallowInput()
|
|
108
|
+
const onExit = () => {
|
|
109
|
+
clear()
|
|
110
|
+
release()
|
|
111
|
+
}
|
|
106
112
|
// Interrupted, the block goes before the shell's prompt comes back.
|
|
107
113
|
const signals = holdSignals(stop)
|
|
108
114
|
|
|
@@ -110,6 +116,7 @@ export function liveRegion(render: (spinner: string) => string[], stream: Progre
|
|
|
110
116
|
clearInterval(timer)
|
|
111
117
|
clear()
|
|
112
118
|
restore()
|
|
119
|
+
release()
|
|
113
120
|
process.off('exit', onExit)
|
|
114
121
|
signals.release()
|
|
115
122
|
}
|
|
@@ -288,6 +295,35 @@ function cutToWidth(line: string, width: number): string {
|
|
|
288
295
|
return `${kept}${RESET_STYLE}${linked ? CLOSE_LINK : ''}`
|
|
289
296
|
}
|
|
290
297
|
|
|
298
|
+
// Keeps keys typed on the terminal from being echoed while the region is drawn: an Enter would push it down a line and leave its first line behind on every redraw. Ctrl+C still interrupts.
|
|
299
|
+
function swallowInput(): () => void {
|
|
300
|
+
const input = process.stdin
|
|
301
|
+
|
|
302
|
+
if (!input.isTTY)
|
|
303
|
+
return () => undefined
|
|
304
|
+
|
|
305
|
+
const onData = (data: Buffer) => {
|
|
306
|
+
if (data.includes(CTRL_C))
|
|
307
|
+
process.kill(process.pid, 'SIGINT')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
input.setRawMode(true)
|
|
311
|
+
input.on('data', onData)
|
|
312
|
+
input.resume()
|
|
313
|
+
|
|
314
|
+
let released = false
|
|
315
|
+
|
|
316
|
+
return () => {
|
|
317
|
+
if (released)
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
released = true
|
|
321
|
+
input.off('data', onData)
|
|
322
|
+
input.pause()
|
|
323
|
+
input.setRawMode(false)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
291
327
|
// Clears the region before anything the console or a stream writes, the caller's own code meanwhile included, and answers how to put them back.
|
|
292
328
|
function interceptOutput(clear: () => void): () => void {
|
|
293
329
|
const targets = [...CONSOLE_METHODS.map(name => [console, name] as const), [process.stdout, 'write'] as const, [process.stderr, 'write'] as const]
|
package/src/test.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
1
4
|
import process from 'node:process'
|
|
2
5
|
import { format } from 'node:util'
|
|
6
|
+
import { afterAll } from 'bun:test'
|
|
3
7
|
|
|
4
8
|
export interface Captured<T> {
|
|
5
9
|
result: T
|
|
@@ -9,6 +13,24 @@ export interface Captured<T> {
|
|
|
9
13
|
err: string
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
// Called at the top of a test file, answers a maker of fresh folders under the system temp folder, each removed with its content once the file's tests are done.
|
|
17
|
+
export function tempDirs(): (prefix?: string) => string {
|
|
18
|
+
const made: string[] = []
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
for (const dir of made.splice(0))
|
|
22
|
+
rmSync(dir, { recursive: true, force: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return (prefix = 'test-') => {
|
|
26
|
+
const dir = mkdtempSync(join(tmpdir(), prefix))
|
|
27
|
+
|
|
28
|
+
made.push(dir)
|
|
29
|
+
|
|
30
|
+
return dir
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
// 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
35
|
export async function capture<T>(run: () => Promise<T> | T): Promise<Captured<T>> {
|
|
14
36
|
const written = { out: '', err: '' }
|