@kaynooo/cli-kit 0.9.0 → 0.11.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 +7 -1
- package/package.json +1 -1
- package/src/args.ts +39 -8
- package/src/ci.ts +132 -0
- package/src/cli.ts +78 -37
- package/src/config.ts +11 -5
- package/src/git.ts +55 -0
- package/src/index.ts +5 -0
- package/src/logs.ts +72 -0
- package/src/process.ts +113 -12
- package/src/secrets.ts +10 -0
- package/src/shell.ts +467 -35
- package/src/text.ts +12 -0
- package/src/tool.ts +71 -0
package/src/text.ts
CHANGED
|
@@ -19,3 +19,15 @@ export function distance(a: string, b: string): number {
|
|
|
19
19
|
|
|
20
20
|
return previous[b.length] ?? 0
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
// A name the typed one starts, else one within two edits (one for a name under four letters).
|
|
24
|
+
export function closest(name: string, names: string[]): string | undefined {
|
|
25
|
+
const started = names.find(known => known.startsWith(name))
|
|
26
|
+
|
|
27
|
+
if (started !== undefined)
|
|
28
|
+
return started
|
|
29
|
+
|
|
30
|
+
const [best] = names.map(known => ({ known, far: distance(name, known) })).sort((a, b) => a.far - b.far)
|
|
31
|
+
|
|
32
|
+
return best !== undefined && best.far <= (name.length < 4 ? 1 : 2) ? best.known : undefined
|
|
33
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RunSettings } from './process'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { formatDuration } from './format'
|
|
4
|
+
import { runCommand } from './process'
|
|
5
|
+
|
|
6
|
+
export interface ToolRun {
|
|
7
|
+
code: number
|
|
8
|
+
stdout: string
|
|
9
|
+
stderr: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// What an answer says when retrying it can only give the same answer: an expired token, a missing permission, a route or repository that does not exist.
|
|
13
|
+
const PERMANENT = /\bHTTP (?:401|403|404)\b|unauthorized|forbidden|not found|couldn't be found|token is required/i
|
|
14
|
+
|
|
15
|
+
// True once the directory the process runs in was deleted, as a worktree is after its work lands.
|
|
16
|
+
export function cwdGone(): boolean {
|
|
17
|
+
try {
|
|
18
|
+
process.cwd()
|
|
19
|
+
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class SpawnError extends Error {
|
|
28
|
+
constructor(message: string, readonly missing: boolean) {
|
|
29
|
+
super(message)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A spawn failing before the command runs says only "No such file or directory", which names the working directory once it was deleted.
|
|
34
|
+
export function spawnFailure(command: string, error: unknown): Error {
|
|
35
|
+
const gone = cwdGone()
|
|
36
|
+
const missing = !gone && (error as { code?: unknown } | undefined)?.code === 'ENOENT'
|
|
37
|
+
|
|
38
|
+
return new SpawnError(`${command}: ${error instanceof Error ? error.message : String(error)}${gone ? ' (the working directory was removed)' : ''}`, missing)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// True for a failure a poll cannot recover from: a binary that is not installed, or an answer of 401, 403 or 404.
|
|
42
|
+
export function isPermanentFailure(error: unknown): boolean {
|
|
43
|
+
if (!(error instanceof Error))
|
|
44
|
+
return false
|
|
45
|
+
|
|
46
|
+
return (error instanceof SpawnError && error.missing) || PERMANENT.test(error.message)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Runs a command and returns its code and both streams; throws, naming `label`, when it cannot start or outlives `timeoutMs`.
|
|
50
|
+
export async function runTool(label: string, argv: string[], { timeoutMs, cwd, onStart }: { [K in 'timeoutMs' | 'cwd' | 'onStart']?: RunSettings[K] | undefined } = {}): Promise<ToolRun> {
|
|
51
|
+
let ran
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// The environment is passed whole so the program is looked up on the PATH as it is now, not as the process started with.
|
|
55
|
+
ran = await runCommand(argv, { env: process.env, ...(timeoutMs === undefined ? {} : { timeoutMs }), ...(cwd === undefined ? {} : { cwd }), ...(onStart === undefined ? {} : { onStart }) })
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw spawnFailure(label, error)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ran.timedOut)
|
|
62
|
+
throw new Error(`${label}: no answer in ${formatDuration(timeoutMs ?? 0)}`)
|
|
63
|
+
|
|
64
|
+
const decoder = new TextDecoder()
|
|
65
|
+
const streams = { stdout: '', stderr: '' }
|
|
66
|
+
|
|
67
|
+
for (const { stream, data } of ran.chunks)
|
|
68
|
+
streams[stream] += decoder.decode(data, { stream: true })
|
|
69
|
+
|
|
70
|
+
return { code: ran.code, ...streams }
|
|
71
|
+
}
|