@hardfin/cli 0.1.0-dev.22 → 0.1.0-dev.24
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 +29 -5
- package/dist/cli.js +605 -150
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -224,13 +224,37 @@ environment, so `hardfin login` works out of the box.
|
|
|
224
224
|
## Completing commands in your shell
|
|
225
225
|
|
|
226
226
|
```sh
|
|
227
|
-
|
|
228
|
-
hardfin completion zsh >> ~/.zshrc # every shell after this one
|
|
227
|
+
hardfin completion install # every shell after this one
|
|
229
228
|
```
|
|
230
229
|
|
|
231
|
-
`
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
`install` finds your shell from `$SHELL`, shows what it will change, and asks before changing
|
|
231
|
+
it. Name the shell to install for another one, and pass `--yes` to skip the question.
|
|
232
|
+
|
|
233
|
+
| Shell | The script is written to | And loaded from |
|
|
234
|
+
| ---------- | ----------------------------------------- | ------------------------------------------ |
|
|
235
|
+
| bash | `~/.local/share/hardfin/completion.bash` | `~/.bashrc`, or `~/.bash_profile` on macOS |
|
|
236
|
+
| zsh | `~/.local/share/hardfin/completion.zsh` | `~/.zshrc` |
|
|
237
|
+
| fish | `~/.config/fish/completions/hardfin.fish` | fish, by itself |
|
|
238
|
+
| PowerShell | `~/.local/share/hardfin/completion.ps1` | the profile PowerShell reports |
|
|
239
|
+
|
|
240
|
+
The lines added to a startup file sit between `# >>> hardfin completion >>>` and
|
|
241
|
+
`# <<< hardfin completion <<<`. Installing again replaces them, and
|
|
242
|
+
`hardfin completion uninstall` removes them with the script. Install again after upgrading, so
|
|
243
|
+
the script matches the CLI.
|
|
244
|
+
|
|
245
|
+
To complete in the current shell without installing anything:
|
|
246
|
+
|
|
247
|
+
```sh
|
|
248
|
+
source <(hardfin completion zsh)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The script asks the CLI what may follow what you have typed, so completions never fall behind
|
|
252
|
+
the commands, including the ones generated from the API document. It offers commands, flags,
|
|
253
|
+
the values a flag accepts, and file paths where a flag takes one. zsh, fish and PowerShell
|
|
254
|
+
show a description beside each.
|
|
255
|
+
|
|
256
|
+
The script calls `hardfin` by name, so it needs `hardfin` on your `PATH`. Running
|
|
257
|
+
`node dist/cli.js` from a checkout leaves nothing for it to call, and `npm link` fixes that.
|
|
234
258
|
|
|
235
259
|
## Serving an agent
|
|
236
260
|
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { chmodSync, closeSync, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
6
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { accessSync, chmodSync, closeSync, constants, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
6
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
9
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
10
11
|
import { createServer } from "node:http";
|
|
11
|
-
import { createInterface } from "node:readline";
|
|
12
|
+
import { createInterface as createInterface$1 } from "node:readline";
|
|
12
13
|
//#region src/command/registry.ts
|
|
13
14
|
/** ExitCode is what the process returns, and what an agent branches on. */
|
|
14
15
|
const ExitCode = {
|
|
@@ -21,6 +22,18 @@ const ExitCode = {
|
|
|
21
22
|
function defineCommand(command) {
|
|
22
23
|
return command;
|
|
23
24
|
}
|
|
25
|
+
/** GLOBAL_FLAGS are the options the CLI takes before any command. */
|
|
26
|
+
const GLOBAL_FLAGS = [{
|
|
27
|
+
name: "api-url",
|
|
28
|
+
description: "The API to call, whose host also holds the authorization server",
|
|
29
|
+
valueName: "url",
|
|
30
|
+
schema: z.string()
|
|
31
|
+
}, {
|
|
32
|
+
name: "issuer-url",
|
|
33
|
+
description: "The authorization server, when it does not sit at the API's host",
|
|
34
|
+
valueName: "url",
|
|
35
|
+
schema: z.string()
|
|
36
|
+
}];
|
|
24
37
|
//#endregion
|
|
25
38
|
//#region src/config/settings.ts
|
|
26
39
|
/** The API version this build was written against, sent on every request. */
|
|
@@ -956,104 +969,54 @@ function toBody$1(source) {
|
|
|
956
969
|
}
|
|
957
970
|
}
|
|
958
971
|
//#endregion
|
|
959
|
-
//#region src/
|
|
972
|
+
//#region src/auth/browser.ts
|
|
973
|
+
/** isWSL reports whether this Linux is running under Windows. */
|
|
974
|
+
function isWSL() {
|
|
975
|
+
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* toOpeners lists the ways to open a URL here, best first. WSL needs Windows to do it,
|
|
979
|
+
* because the browser the person is looking at is the Windows one, and xdg-open reaches
|
|
980
|
+
* nothing outside the distribution.
|
|
981
|
+
*/
|
|
982
|
+
function toOpeners(url) {
|
|
983
|
+
if (process.platform === "darwin") return [["open", [url]]];
|
|
984
|
+
if (process.platform === "win32") return [["cmd", [
|
|
985
|
+
"/c",
|
|
986
|
+
"start",
|
|
987
|
+
"",
|
|
988
|
+
url
|
|
989
|
+
]]];
|
|
990
|
+
if (isWSL()) return [
|
|
991
|
+
["wslview", [url]],
|
|
992
|
+
["powershell.exe", [
|
|
993
|
+
"-NoProfile",
|
|
994
|
+
"-NonInteractive",
|
|
995
|
+
"-Command",
|
|
996
|
+
"Start-Process",
|
|
997
|
+
`'${url}'`
|
|
998
|
+
]],
|
|
999
|
+
["explorer.exe", [url]]
|
|
1000
|
+
];
|
|
1001
|
+
return [["xdg-open", [url]]];
|
|
1002
|
+
}
|
|
1003
|
+
/** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
|
|
1004
|
+
function openBrowser(url, launch = spawnSync) {
|
|
1005
|
+
for (const [command, args] of toOpeners(url)) if (launch(command, args, {
|
|
1006
|
+
stdio: "ignore",
|
|
1007
|
+
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
1008
|
+
timeout: 1e4
|
|
1009
|
+
}).error === void 0) return true;
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/command/completion-scripts.ts
|
|
960
1014
|
const SHELLS = [
|
|
961
1015
|
"bash",
|
|
962
1016
|
"zsh",
|
|
963
1017
|
"fish",
|
|
964
1018
|
"powershell"
|
|
965
1019
|
];
|
|
966
|
-
const completionCommand = defineCommand({
|
|
967
|
-
name: "completion",
|
|
968
|
-
summary: "Print the shell script that completes hardfin commands",
|
|
969
|
-
description: "Writes a script for your shell. The script asks this CLI what may follow what you have typed, so completions never fall behind the commands.",
|
|
970
|
-
arguments: [{
|
|
971
|
-
name: "shell",
|
|
972
|
-
description: `The shell to write for: ${SHELLS.join(", ")}`,
|
|
973
|
-
required: true
|
|
974
|
-
}],
|
|
975
|
-
flags: [],
|
|
976
|
-
examples: [
|
|
977
|
-
{
|
|
978
|
-
description: "Complete in this shell, now",
|
|
979
|
-
command: "source <(hardfin completion zsh)"
|
|
980
|
-
},
|
|
981
|
-
{
|
|
982
|
-
description: "Complete in every new shell",
|
|
983
|
-
command: "hardfin completion zsh > ~/.hardfin-completion.zsh"
|
|
984
|
-
},
|
|
985
|
-
{
|
|
986
|
-
description: "Complete in bash",
|
|
987
|
-
command: "source <(hardfin completion bash)"
|
|
988
|
-
}
|
|
989
|
-
],
|
|
990
|
-
run: runCompletion
|
|
991
|
-
});
|
|
992
|
-
/** The hidden command a completion script asks, which keeps one implementation for every shell. */
|
|
993
|
-
const completeCommand = defineCommand({
|
|
994
|
-
name: "__complete",
|
|
995
|
-
summary: "Answer what may follow the words typed so far",
|
|
996
|
-
hidden: true,
|
|
997
|
-
arguments: [{
|
|
998
|
-
name: "words",
|
|
999
|
-
description: "The words typed so far",
|
|
1000
|
-
required: false,
|
|
1001
|
-
variadic: true
|
|
1002
|
-
}],
|
|
1003
|
-
flags: [{
|
|
1004
|
-
name: "json",
|
|
1005
|
-
description: "Accepted for consistency, and ignored",
|
|
1006
|
-
schema: z.boolean()
|
|
1007
|
-
}],
|
|
1008
|
-
examples: [],
|
|
1009
|
-
run: async (input) => {
|
|
1010
|
-
writeData(toCandidates(input.commands, input.args).join("\n"));
|
|
1011
|
-
return ExitCode.OK;
|
|
1012
|
-
}
|
|
1013
|
-
});
|
|
1014
|
-
/**
|
|
1015
|
-
* toCandidates answers what may follow the words typed so far. A word starting with a dash
|
|
1016
|
-
* asks for the current command's flags, and anything else asks for its subcommands.
|
|
1017
|
-
*/
|
|
1018
|
-
function toCandidates(commands, words) {
|
|
1019
|
-
const partial = words[words.length - 1] ?? "";
|
|
1020
|
-
const walked = toWalked(commands, words.slice(0, -1));
|
|
1021
|
-
if (partial.startsWith("-")) return toFlagNames(walked.command).filter((name) => name.startsWith(partial));
|
|
1022
|
-
if (walked.isUnknown) return [];
|
|
1023
|
-
return (walked.command?.subcommands ?? walked.remaining).map((command) => command.name).filter((name) => !name.startsWith("__")).filter((name) => name.startsWith(partial));
|
|
1024
|
-
}
|
|
1025
|
-
function toWalked(commands, words) {
|
|
1026
|
-
let remaining = commands;
|
|
1027
|
-
let command;
|
|
1028
|
-
for (const word of words) {
|
|
1029
|
-
if (word.startsWith("-") || command?.arguments.length) continue;
|
|
1030
|
-
const found = remaining.find((entry) => entry.name === word);
|
|
1031
|
-
if (!found) return {
|
|
1032
|
-
command,
|
|
1033
|
-
remaining,
|
|
1034
|
-
isUnknown: true
|
|
1035
|
-
};
|
|
1036
|
-
command = found;
|
|
1037
|
-
remaining = found.subcommands ?? [];
|
|
1038
|
-
}
|
|
1039
|
-
return {
|
|
1040
|
-
command,
|
|
1041
|
-
remaining,
|
|
1042
|
-
isUnknown: false
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
function toFlagNames(command) {
|
|
1046
|
-
return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
|
|
1047
|
-
}
|
|
1048
|
-
async function runCompletion(input) {
|
|
1049
|
-
const shell = input.args[0] ?? "";
|
|
1050
|
-
if (!SHELLS.includes(shell)) {
|
|
1051
|
-
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
1052
|
-
return ExitCode.USAGE;
|
|
1053
|
-
}
|
|
1054
|
-
writeData(toScript(shell));
|
|
1055
|
-
return ExitCode.OK;
|
|
1056
|
-
}
|
|
1057
1020
|
/** toScript writes a shell's completion, each one asking __complete for the candidates. */
|
|
1058
1021
|
function toScript(shell) {
|
|
1059
1022
|
return SCRIPTS[shell];
|
|
@@ -1087,14 +1050,28 @@ _hardfin_words() {
|
|
|
1087
1050
|
}
|
|
1088
1051
|
|
|
1089
1052
|
_hardfin_complete() {
|
|
1090
|
-
local _hardfin_line_words
|
|
1053
|
+
local _hardfin_line_words line directive current prefix breaks
|
|
1091
1054
|
_hardfin_words
|
|
1092
1055
|
COMPREPLY=()
|
|
1093
1056
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1057
|
+
# Each line is a candidate with its description after a tab, which bash cannot show, and
|
|
1058
|
+
# the last line is the directive.
|
|
1059
|
+
while IFS= read -r line; do
|
|
1060
|
+
if [[ $line == :* ]]; then
|
|
1061
|
+
directive=$line
|
|
1062
|
+
else
|
|
1063
|
+
COMPREPLY+=("\${line%%$'\\t'*}")
|
|
1064
|
+
fi
|
|
1096
1065
|
done < <(hardfin __complete -- "\${_hardfin_line_words[@]:1}" 2>/dev/null)
|
|
1097
1066
|
|
|
1067
|
+
# A path is left to readline, which completes it from the file system.
|
|
1068
|
+
if [[ $directive == :1 ]]; then
|
|
1069
|
+
compopt -o default 2>/dev/null
|
|
1070
|
+
COMPREPLY=()
|
|
1071
|
+
|
|
1072
|
+
return
|
|
1073
|
+
fi
|
|
1074
|
+
|
|
1098
1075
|
# Readline replaces only the text after the last = or : in the word, so a candidate
|
|
1099
1076
|
# loses what comes before it or the line would repeat that part.
|
|
1100
1077
|
current=\${_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]}
|
|
@@ -1106,21 +1083,68 @@ _hardfin_complete() {
|
|
|
1106
1083
|
fi
|
|
1107
1084
|
}
|
|
1108
1085
|
|
|
1109
|
-
|
|
1086
|
+
# Bash before 4.0 has no compopt, so files are offered whenever no candidate is.
|
|
1087
|
+
if type compopt >/dev/null 2>&1; then
|
|
1088
|
+
complete -F _hardfin_complete hardfin
|
|
1089
|
+
else
|
|
1090
|
+
complete -o default -F _hardfin_complete hardfin
|
|
1091
|
+
fi`,
|
|
1110
1092
|
zsh: `# hardfin completion for zsh
|
|
1111
1093
|
_hardfin_complete() {
|
|
1112
|
-
local -a
|
|
1113
|
-
|
|
1114
|
-
|
|
1094
|
+
local -a lines described
|
|
1095
|
+
local line name directive
|
|
1096
|
+
lines=(\${(f)"$(hardfin __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)"})
|
|
1097
|
+
directive=\${lines[-1]}
|
|
1098
|
+
lines=(\${lines[1,-2]})
|
|
1099
|
+
|
|
1100
|
+
if [[ $directive == :1 ]]; then
|
|
1101
|
+
_files
|
|
1102
|
+
|
|
1103
|
+
return
|
|
1104
|
+
fi
|
|
1105
|
+
|
|
1106
|
+
# Each line is a candidate with its description after a tab, and _describe separates the
|
|
1107
|
+
# two with a colon, so a colon in the candidate is escaped.
|
|
1108
|
+
for line in $lines; do
|
|
1109
|
+
name=\${line%%$'\\t'*}
|
|
1110
|
+
name=\${name//:/\\\\:}
|
|
1111
|
+
|
|
1112
|
+
if [[ $line == *$'\\t'* ]]; then
|
|
1113
|
+
described+=("$name:\${line#*$'\\t'}")
|
|
1114
|
+
else
|
|
1115
|
+
described+=("$name")
|
|
1116
|
+
fi
|
|
1117
|
+
done
|
|
1118
|
+
|
|
1119
|
+
_describe 'hardfin' described -M 'm:{a-zA-Z}={A-Za-z}'
|
|
1115
1120
|
}
|
|
1116
1121
|
|
|
1122
|
+
# compdef comes from compinit, which a startup file without completion never runs.
|
|
1123
|
+
if ! (( $+functions[compdef] )); then
|
|
1124
|
+
autoload -Uz compinit && compinit
|
|
1125
|
+
fi
|
|
1126
|
+
|
|
1117
1127
|
compdef _hardfin_complete hardfin`,
|
|
1118
1128
|
fish: `# hardfin completion for fish
|
|
1119
1129
|
function __hardfin_complete
|
|
1120
1130
|
# Older fish lacks -x, which replaced -o, so it falls back to the older option.
|
|
1121
1131
|
set -l words (commandline -xpc 2>/dev/null; or commandline -opc)
|
|
1122
1132
|
set -l current (commandline -ct)
|
|
1123
|
-
hardfin __complete -- $words[2..-1] "$current" 2>/dev/null
|
|
1133
|
+
set -l lines (hardfin __complete -- $words[2..-1] "$current" 2>/dev/null)
|
|
1134
|
+
set -l directive $lines[-1]
|
|
1135
|
+
|
|
1136
|
+
if set -q lines[1]
|
|
1137
|
+
set -e lines[-1]
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
if test "$directive" = ":1"
|
|
1141
|
+
__fish_complete_path "$current"
|
|
1142
|
+
|
|
1143
|
+
return
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Fish shows what follows a tab as the description, so candidates pass through as they are.
|
|
1147
|
+
string join \\n -- $lines
|
|
1124
1148
|
end
|
|
1125
1149
|
|
|
1126
1150
|
complete -c hardfin -f -a "(__hardfin_complete)"`,
|
|
@@ -1139,18 +1163,488 @@ Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
|
1139
1163
|
$words += if ($isLegacy) { '""' } else { '' }
|
|
1140
1164
|
}
|
|
1141
1165
|
|
|
1142
|
-
hardfin __complete -- @words 2>$null
|
|
1143
|
-
|
|
1166
|
+
$lines = @(hardfin __complete -- @words 2>$null)
|
|
1167
|
+
|
|
1168
|
+
# Returning nothing makes PowerShell complete paths, which is what the last line asks for
|
|
1169
|
+
# when it is :1.
|
|
1170
|
+
if ($lines.Count -eq 0 -or $lines[-1] -eq ':1') {
|
|
1171
|
+
return
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
# Each line is a candidate with its description after a tab.
|
|
1175
|
+
$lines | Select-Object -SkipLast 1 | ForEach-Object {
|
|
1176
|
+
$value, $description = $_ -split "\`t", 2
|
|
1177
|
+
$text = $value
|
|
1144
1178
|
|
|
1145
1179
|
if ($text -match '[\\s#@$;,''{}()"\`|<>&]') {
|
|
1146
1180
|
$text = "'" + ($text -replace "'", "''") + "'"
|
|
1147
1181
|
}
|
|
1148
1182
|
|
|
1149
|
-
[System.Management.Automation.CompletionResult]::new($text, $
|
|
1183
|
+
[System.Management.Automation.CompletionResult]::new($text, $value, 'ParameterValue', $(if ($description) { $description } else { $value }))
|
|
1150
1184
|
}
|
|
1151
1185
|
}`
|
|
1152
1186
|
};
|
|
1153
1187
|
//#endregion
|
|
1188
|
+
//#region src/command/completion-install.ts
|
|
1189
|
+
const BLOCK_START = "# >>> hardfin completion >>>";
|
|
1190
|
+
const BLOCK_END = "# <<< hardfin completion <<<";
|
|
1191
|
+
const SHELL_ARGUMENT = {
|
|
1192
|
+
name: "shell",
|
|
1193
|
+
description: `The shell to install for, found from $SHELL when not given: ${SHELLS.join(", ")}`,
|
|
1194
|
+
required: false,
|
|
1195
|
+
values: [...SHELLS]
|
|
1196
|
+
};
|
|
1197
|
+
const YES_FLAG = {
|
|
1198
|
+
name: "yes",
|
|
1199
|
+
short: "y",
|
|
1200
|
+
description: "Make the change without asking",
|
|
1201
|
+
schema: z.boolean()
|
|
1202
|
+
};
|
|
1203
|
+
const JSON_FLAG$1 = {
|
|
1204
|
+
name: "json",
|
|
1205
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1206
|
+
schema: z.boolean()
|
|
1207
|
+
};
|
|
1208
|
+
const installCommand = defineCommand({
|
|
1209
|
+
name: "install",
|
|
1210
|
+
summary: "Set up completion in your shell",
|
|
1211
|
+
description: "Writes the completion script under your data directory and loads it from your shell's startup file, between two marked lines. Fish loads it from its completions directory, so no startup file changes. Run it again after upgrading, so the script matches the CLI.",
|
|
1212
|
+
arguments: [SHELL_ARGUMENT],
|
|
1213
|
+
flags: [YES_FLAG, JSON_FLAG$1],
|
|
1214
|
+
examples: [{
|
|
1215
|
+
description: "Install for the shell you are using",
|
|
1216
|
+
command: "hardfin completion install"
|
|
1217
|
+
}, {
|
|
1218
|
+
description: "Install for zsh without being asked",
|
|
1219
|
+
command: "hardfin completion install zsh --yes"
|
|
1220
|
+
}],
|
|
1221
|
+
run: (input) => runChange(input, "install")
|
|
1222
|
+
});
|
|
1223
|
+
const uninstallCommand = defineCommand({
|
|
1224
|
+
name: "uninstall",
|
|
1225
|
+
summary: "Remove completion from your shell",
|
|
1226
|
+
description: "Removes the script install wrote, and the marked lines it added to your shell's startup file.",
|
|
1227
|
+
arguments: [SHELL_ARGUMENT],
|
|
1228
|
+
flags: [YES_FLAG, JSON_FLAG$1],
|
|
1229
|
+
examples: [{
|
|
1230
|
+
description: "Remove completion from zsh",
|
|
1231
|
+
command: "hardfin completion uninstall zsh"
|
|
1232
|
+
}],
|
|
1233
|
+
run: (input) => runChange(input, "uninstall")
|
|
1234
|
+
});
|
|
1235
|
+
/**
|
|
1236
|
+
* toPlan decides where a shell's completion goes. Fish loads a file from its completions
|
|
1237
|
+
* directory by itself, and every other shell is told to load the script from its startup
|
|
1238
|
+
* file, because not every bash has bash-completion to load it lazily.
|
|
1239
|
+
*/
|
|
1240
|
+
function toPlan(shell, place) {
|
|
1241
|
+
const content = `${toScript(shell)}\n`;
|
|
1242
|
+
if (shell === "fish") {
|
|
1243
|
+
const config = place.env["XDG_CONFIG_HOME"] || join(place.home, ".config");
|
|
1244
|
+
return {
|
|
1245
|
+
shell,
|
|
1246
|
+
script: {
|
|
1247
|
+
path: join(config, "fish", "completions", "hardfin.fish"),
|
|
1248
|
+
content
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const data = toDataDirectory(place);
|
|
1253
|
+
const extension = {
|
|
1254
|
+
bash: "bash",
|
|
1255
|
+
zsh: "zsh",
|
|
1256
|
+
powershell: "ps1"
|
|
1257
|
+
}[shell];
|
|
1258
|
+
const path = join(data, `completion.${extension}`);
|
|
1259
|
+
if (shell === "powershell") {
|
|
1260
|
+
const profile = place.toProfile();
|
|
1261
|
+
const block = `if (Test-Path '${path}') { . '${path}' }`;
|
|
1262
|
+
return {
|
|
1263
|
+
shell,
|
|
1264
|
+
script: {
|
|
1265
|
+
path,
|
|
1266
|
+
content
|
|
1267
|
+
},
|
|
1268
|
+
startup: profile ? {
|
|
1269
|
+
path: profile,
|
|
1270
|
+
block
|
|
1271
|
+
} : void 0
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
const block = `[ -f '${path}' ] && . '${path}'`;
|
|
1275
|
+
return {
|
|
1276
|
+
shell,
|
|
1277
|
+
script: {
|
|
1278
|
+
path,
|
|
1279
|
+
content
|
|
1280
|
+
},
|
|
1281
|
+
startup: {
|
|
1282
|
+
path: toStartupFile(shell, place),
|
|
1283
|
+
block
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/** toDataDirectory is where hardfin keeps files that are not settings or credentials. */
|
|
1288
|
+
function toDataDirectory(place) {
|
|
1289
|
+
if (place.platform === "win32") return join(place.env["LOCALAPPDATA"] || join(place.home, "AppData", "Local"), "hardfin");
|
|
1290
|
+
return join(place.env["XDG_DATA_HOME"] || join(place.home, ".local", "share"), "hardfin");
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* toStartupFile names the file a shell reads when a person opens one. A terminal on macOS
|
|
1294
|
+
* starts bash as a login shell, which reads .bash_profile and never .bashrc.
|
|
1295
|
+
*/
|
|
1296
|
+
function toStartupFile(shell, place) {
|
|
1297
|
+
if (shell === "zsh") return join(place.env["ZDOTDIR"] || place.home, ".zshrc");
|
|
1298
|
+
return join(place.home, place.platform === "darwin" ? ".bash_profile" : ".bashrc");
|
|
1299
|
+
}
|
|
1300
|
+
/** withBlock answers a startup file holding the marked block once, replacing any earlier copy. */
|
|
1301
|
+
function withBlock(text, block) {
|
|
1302
|
+
const marked = `${BLOCK_START}\n${block}\n${BLOCK_END}`;
|
|
1303
|
+
const without = withoutBlock(text);
|
|
1304
|
+
if (without === "") return `${marked}\n`;
|
|
1305
|
+
return `${without.endsWith("\n") ? without : `${without}\n`}\n${marked}\n`;
|
|
1306
|
+
}
|
|
1307
|
+
/** withoutBlock answers a startup file with the marked block removed, and the blank line before it. */
|
|
1308
|
+
function withoutBlock(text) {
|
|
1309
|
+
const start = text.indexOf(BLOCK_START);
|
|
1310
|
+
const end = text.indexOf(BLOCK_END, start);
|
|
1311
|
+
if (start === -1 || end === -1) return text;
|
|
1312
|
+
return text.slice(0, start).replace(/\n\n$/, "\n") + text.slice(end + 28).replace(/^\n/, "");
|
|
1313
|
+
}
|
|
1314
|
+
/** toShell finds the shell a person is using, from what the environment says. */
|
|
1315
|
+
function toShell(place) {
|
|
1316
|
+
const name = basename(place.env["SHELL"] ?? "").replace(/\.exe$/, "");
|
|
1317
|
+
if (SHELLS.includes(name)) return name;
|
|
1318
|
+
if (name === "pwsh") return "powershell";
|
|
1319
|
+
if (place.platform === "win32") return "powershell";
|
|
1320
|
+
}
|
|
1321
|
+
/** isInstalled answers whether install has written a script for any shell. */
|
|
1322
|
+
function isInstalled(place = toPlace()) {
|
|
1323
|
+
return SHELLS.some((shell) => existsSync(toPlan(shell, {
|
|
1324
|
+
...place,
|
|
1325
|
+
toProfile: () => void 0
|
|
1326
|
+
}).script.path));
|
|
1327
|
+
}
|
|
1328
|
+
function toPlace() {
|
|
1329
|
+
return {
|
|
1330
|
+
home: homedir(),
|
|
1331
|
+
platform: process.platform,
|
|
1332
|
+
env: process.env,
|
|
1333
|
+
toProfile: toPowerShellProfile
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
/** toPowerShellProfile asks PowerShell where its profile is, because it moves between versions. */
|
|
1337
|
+
function toPowerShellProfile() {
|
|
1338
|
+
for (const program of ["pwsh", "powershell"]) {
|
|
1339
|
+
const result = spawnSync(program, [
|
|
1340
|
+
"-NoProfile",
|
|
1341
|
+
"-NoLogo",
|
|
1342
|
+
"-Command",
|
|
1343
|
+
"$PROFILE.CurrentUserAllHosts"
|
|
1344
|
+
], { encoding: "utf8" });
|
|
1345
|
+
const profile = result.status === 0 ? result.stdout.trim() : "";
|
|
1346
|
+
if (profile !== "") return profile;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function runChange(input, change) {
|
|
1350
|
+
const place = toPlace();
|
|
1351
|
+
const shell = input.args[0] ?? toShell(place);
|
|
1352
|
+
if (!shell || !SHELLS.includes(shell)) {
|
|
1353
|
+
writeFailure(`name the shell to ${change} for: ${SHELLS.join(", ")}`, input.isJSON);
|
|
1354
|
+
return ExitCode.USAGE;
|
|
1355
|
+
}
|
|
1356
|
+
const plan = toPlan(shell, place);
|
|
1357
|
+
if (change === "install" && shell === "powershell" && !plan.startup) {
|
|
1358
|
+
writeFailure("PowerShell did not report its profile, so there is nowhere to load the script from", input.isJSON);
|
|
1359
|
+
return ExitCode.ERROR;
|
|
1360
|
+
}
|
|
1361
|
+
process.stderr.write(toDescription(plan, change));
|
|
1362
|
+
if (!await isConfirmed(input.flags["yes"] === true)) {
|
|
1363
|
+
writeFailure(process.stdin.isTTY ? "nothing was changed" : "nothing was changed. Pass --yes to change it without a terminal", input.isJSON);
|
|
1364
|
+
return ExitCode.USAGE;
|
|
1365
|
+
}
|
|
1366
|
+
if (change === "install") {
|
|
1367
|
+
mkdirSync(dirname(plan.script.path), { recursive: true });
|
|
1368
|
+
writeFileSync(plan.script.path, plan.script.content);
|
|
1369
|
+
if (plan.startup) {
|
|
1370
|
+
const text = existsSync(plan.startup.path) ? readFileSync(plan.startup.path, "utf8") : "";
|
|
1371
|
+
mkdirSync(dirname(plan.startup.path), { recursive: true });
|
|
1372
|
+
writeFileSync(plan.startup.path, withBlock(text, plan.startup.block));
|
|
1373
|
+
}
|
|
1374
|
+
for (const warning of toWarnings(place)) process.stderr.write(`warning: ${warning}\n`);
|
|
1375
|
+
} else {
|
|
1376
|
+
rmSync(plan.script.path, { force: true });
|
|
1377
|
+
if (plan.startup && existsSync(plan.startup.path)) writeFileSync(plan.startup.path, withoutBlock(readFileSync(plan.startup.path, "utf8")));
|
|
1378
|
+
}
|
|
1379
|
+
writeData({
|
|
1380
|
+
shell,
|
|
1381
|
+
[change === "install" ? "installed" : "uninstalled"]: true,
|
|
1382
|
+
script: plan.script.path,
|
|
1383
|
+
startupFile: plan.startup?.path ?? null,
|
|
1384
|
+
next: change === "install" ? "Open a new shell to complete hardfin commands with Tab" : "Open a new shell to stop completing hardfin commands"
|
|
1385
|
+
});
|
|
1386
|
+
return ExitCode.OK;
|
|
1387
|
+
}
|
|
1388
|
+
function toDescription(plan, change) {
|
|
1389
|
+
const lines = change === "install" ? [`This writes the ${plan.shell} completion script to ${plan.script.path}`] : [`This removes the ${plan.shell} completion script at ${plan.script.path}`];
|
|
1390
|
+
if (plan.startup) lines.push(change === "install" ? `and adds these lines to ${plan.startup.path}:\n\n${BLOCK_START}\n${plan.startup.block}\n${BLOCK_END}\n` : `and the lines between the hardfin completion markers in ${plan.startup.path}`);
|
|
1391
|
+
return `${lines.join("\n")}\n`;
|
|
1392
|
+
}
|
|
1393
|
+
/** isConfirmed asks before changing a person's files, unless they already said yes. */
|
|
1394
|
+
async function isConfirmed(isYes) {
|
|
1395
|
+
if (isYes) return true;
|
|
1396
|
+
if (!process.stdin.isTTY) return false;
|
|
1397
|
+
const reader = createInterface({
|
|
1398
|
+
input: process.stdin,
|
|
1399
|
+
output: process.stderr
|
|
1400
|
+
});
|
|
1401
|
+
try {
|
|
1402
|
+
const answer = await reader.question("Continue? [y/N] ");
|
|
1403
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1404
|
+
} finally {
|
|
1405
|
+
reader.close();
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* toWarnings names what would stop the script from working once installed. The script
|
|
1410
|
+
* calls hardfin by name, so a CLI run from a checkout has nothing for it to call.
|
|
1411
|
+
*/
|
|
1412
|
+
function toWarnings(place, find = (name) => toOnPath(name, place), isInsideWSL = isWSL) {
|
|
1413
|
+
const found = find(place.platform === "win32" ? "hardfin.cmd" : "hardfin");
|
|
1414
|
+
if (!found) return ["hardfin is not on your PATH, and the script calls it by name. Install it with npm install -g @hardfin/cli, or npm link in a checkout"];
|
|
1415
|
+
if (place.platform === "linux" && found.startsWith("/mnt/") && isInsideWSL()) return [`the hardfin on your PATH is the Windows one at ${found}. Install it inside WSL, so each Tab runs the Linux one`];
|
|
1416
|
+
return [];
|
|
1417
|
+
}
|
|
1418
|
+
function toOnPath(name, place) {
|
|
1419
|
+
return (place.env["PATH"] ?? "").split(delimiter).filter((directory) => directory !== "").map((directory) => join(directory, name)).find(isExecutable);
|
|
1420
|
+
}
|
|
1421
|
+
function isExecutable(path) {
|
|
1422
|
+
try {
|
|
1423
|
+
accessSync(path, constants.X_OK);
|
|
1424
|
+
return true;
|
|
1425
|
+
} catch {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
//#endregion
|
|
1430
|
+
//#region src/command/completion.ts
|
|
1431
|
+
const completionCommand = defineCommand({
|
|
1432
|
+
name: "completion",
|
|
1433
|
+
summary: "Complete hardfin commands in your shell",
|
|
1434
|
+
description: "Prints the completion script for a shell. The script asks this CLI what may follow what you have typed, so completions never fall behind the commands. Run hardfin completion install to load it in every new shell.",
|
|
1435
|
+
arguments: [{
|
|
1436
|
+
name: "shell",
|
|
1437
|
+
description: `The shell to write for: ${SHELLS.join(", ")}`,
|
|
1438
|
+
required: true,
|
|
1439
|
+
values: [...SHELLS]
|
|
1440
|
+
}],
|
|
1441
|
+
flags: [],
|
|
1442
|
+
examples: [{
|
|
1443
|
+
description: "Complete in every new shell",
|
|
1444
|
+
command: "hardfin completion install"
|
|
1445
|
+
}, {
|
|
1446
|
+
description: "Complete in this shell, now",
|
|
1447
|
+
command: "source <(hardfin completion zsh)"
|
|
1448
|
+
}],
|
|
1449
|
+
subcommands: [installCommand, uninstallCommand],
|
|
1450
|
+
run: runCompletion
|
|
1451
|
+
});
|
|
1452
|
+
/** The hidden command a completion script asks, which keeps one implementation for every shell. */
|
|
1453
|
+
const completeCommand = defineCommand({
|
|
1454
|
+
name: "__complete",
|
|
1455
|
+
summary: "Answer what may follow the words typed so far",
|
|
1456
|
+
hidden: true,
|
|
1457
|
+
arguments: [{
|
|
1458
|
+
name: "words",
|
|
1459
|
+
description: "The words typed so far",
|
|
1460
|
+
required: false,
|
|
1461
|
+
variadic: true
|
|
1462
|
+
}],
|
|
1463
|
+
flags: [{
|
|
1464
|
+
name: "json",
|
|
1465
|
+
description: "Accepted for consistency, and ignored",
|
|
1466
|
+
schema: z.boolean()
|
|
1467
|
+
}],
|
|
1468
|
+
examples: [],
|
|
1469
|
+
run: async (input) => {
|
|
1470
|
+
writeData(toAnswer$1(toCompletion(input.commands, input.args)));
|
|
1471
|
+
return ExitCode.OK;
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
/** Directive is the last line of an answer, which tells the script what to do beyond the candidates. */
|
|
1475
|
+
const Directive = {
|
|
1476
|
+
NONE: ":0",
|
|
1477
|
+
FILES: ":1"
|
|
1478
|
+
};
|
|
1479
|
+
const HELP_FLAG = {
|
|
1480
|
+
name: "help",
|
|
1481
|
+
short: "h",
|
|
1482
|
+
description: "Print help for the command",
|
|
1483
|
+
schema: z.boolean()
|
|
1484
|
+
};
|
|
1485
|
+
const VERSION_FLAG = {
|
|
1486
|
+
name: "version",
|
|
1487
|
+
short: "v",
|
|
1488
|
+
description: "Print the version",
|
|
1489
|
+
schema: z.boolean()
|
|
1490
|
+
};
|
|
1491
|
+
/** The value names that mean a path, so the shell offers files for them. */
|
|
1492
|
+
const PATH_VALUES = /* @__PURE__ */ new Set(["path", "file"]);
|
|
1493
|
+
const NOTHING = {
|
|
1494
|
+
candidates: [],
|
|
1495
|
+
isFile: false
|
|
1496
|
+
};
|
|
1497
|
+
/**
|
|
1498
|
+
* toAnswer writes a completion as a script reads it. Each line is a candidate, with its
|
|
1499
|
+
* description after a tab, and the last line is the directive.
|
|
1500
|
+
*/
|
|
1501
|
+
function toAnswer$1(completion) {
|
|
1502
|
+
return [...completion.candidates.map((candidate) => candidate.description ? `${candidate.value}\t${toLine(candidate.description)}` : candidate.value), completion.isFile ? Directive.FILES : Directive.NONE].join("\n");
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* toCompletion answers what may follow the words typed so far. The last word is what is
|
|
1506
|
+
* being typed, and it is empty when the cursor sits after a space, which asks what may come
|
|
1507
|
+
* next rather than how to finish the word before it.
|
|
1508
|
+
*/
|
|
1509
|
+
function toCompletion(commands, words) {
|
|
1510
|
+
const partial = words[words.length - 1] ?? "";
|
|
1511
|
+
const walked = toWalked(commands, words.slice(0, -1));
|
|
1512
|
+
if (walked.isUnknown) return NOTHING;
|
|
1513
|
+
if (walked.pending) return toValues(walked.pending, partial, "");
|
|
1514
|
+
const assigned = /^(--?[^=]+)=(.*)$/.exec(partial);
|
|
1515
|
+
if (assigned?.[1] !== void 0 && assigned[2] !== void 0) {
|
|
1516
|
+
const flag = toFlag(walked.flags, assigned[1]);
|
|
1517
|
+
return flag?.valueName ? {
|
|
1518
|
+
candidates: toValues(flag, assigned[2], `${assigned[1]}=`).candidates,
|
|
1519
|
+
isFile: false
|
|
1520
|
+
} : NOTHING;
|
|
1521
|
+
}
|
|
1522
|
+
if (partial.startsWith("-")) return {
|
|
1523
|
+
candidates: toFlagCandidates(walked).filter((candidate) => candidate.value.startsWith(partial)),
|
|
1524
|
+
isFile: false
|
|
1525
|
+
};
|
|
1526
|
+
const subcommands = toSubcommands(commands, walked).filter((command) => !command.hidden && command.name.startsWith(partial)).map((command) => ({
|
|
1527
|
+
value: command.name,
|
|
1528
|
+
description: command.summary
|
|
1529
|
+
}));
|
|
1530
|
+
if (walked.command && walked.command.subcommands?.length && !walked.command.run) return {
|
|
1531
|
+
candidates: subcommands,
|
|
1532
|
+
isFile: false
|
|
1533
|
+
};
|
|
1534
|
+
return {
|
|
1535
|
+
candidates: [...subcommands, ...toArgumentValues(walked, partial)],
|
|
1536
|
+
isFile: false
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
/** toSubcommands lists the commands the next word may name. */
|
|
1540
|
+
function toSubcommands(commands, walked) {
|
|
1541
|
+
if (!walked.command) return commands;
|
|
1542
|
+
return walked.positionals === 0 ? walked.command.subcommands ?? [] : [];
|
|
1543
|
+
}
|
|
1544
|
+
function toWalked(commands, words) {
|
|
1545
|
+
let walked = {
|
|
1546
|
+
flags: [
|
|
1547
|
+
...GLOBAL_FLAGS,
|
|
1548
|
+
VERSION_FLAG,
|
|
1549
|
+
HELP_FLAG
|
|
1550
|
+
],
|
|
1551
|
+
used: /* @__PURE__ */ new Set(),
|
|
1552
|
+
positionals: 0,
|
|
1553
|
+
isUnknown: false
|
|
1554
|
+
};
|
|
1555
|
+
for (const word of words) {
|
|
1556
|
+
if (walked.pending) {
|
|
1557
|
+
walked.pending = void 0;
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (word.startsWith("-")) {
|
|
1561
|
+
const flag = toFlag(walked.flags, word.split("=")[0] ?? word);
|
|
1562
|
+
if (flag) walked.used.add(flag.name);
|
|
1563
|
+
if (flag?.valueName && !word.includes("=")) walked.pending = flag;
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const found = toSubcommands(commands, walked).find((entry) => entry.name === word);
|
|
1567
|
+
if (found) {
|
|
1568
|
+
walked = {
|
|
1569
|
+
command: found,
|
|
1570
|
+
flags: [...found.flags, HELP_FLAG],
|
|
1571
|
+
used: /* @__PURE__ */ new Set(),
|
|
1572
|
+
positionals: 0,
|
|
1573
|
+
isUnknown: false
|
|
1574
|
+
};
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (walked.command?.arguments.length) {
|
|
1578
|
+
walked.positionals += 1;
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
...walked,
|
|
1583
|
+
isUnknown: true
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
return walked;
|
|
1587
|
+
}
|
|
1588
|
+
/** toFlag finds the flag a word names, whether long, short, or the --no- form of a boolean. */
|
|
1589
|
+
function toFlag(flags, word) {
|
|
1590
|
+
if (word.startsWith("--")) {
|
|
1591
|
+
const name = word.slice(2);
|
|
1592
|
+
return flags.find((flag) => flag.name === name || flag.negatable && `no-${flag.name}` === name);
|
|
1593
|
+
}
|
|
1594
|
+
return flags.find((flag) => flag.short !== void 0 && `-${flag.short}` === word);
|
|
1595
|
+
}
|
|
1596
|
+
function toFlagCandidates(walked) {
|
|
1597
|
+
return walked.flags.filter((flag) => flag.repeatable || !walked.used.has(flag.name)).flatMap((flag) => {
|
|
1598
|
+
const candidate = {
|
|
1599
|
+
value: `--${flag.name}`,
|
|
1600
|
+
description: flag.description
|
|
1601
|
+
};
|
|
1602
|
+
return flag.negatable ? [candidate, {
|
|
1603
|
+
value: `--no-${flag.name}`,
|
|
1604
|
+
description: `${flag.description}, turned off`
|
|
1605
|
+
}] : [candidate];
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
/** toValues offers the values a flag takes, each written after the prefix the word already has. */
|
|
1609
|
+
function toValues(flag, partial, prefix) {
|
|
1610
|
+
const values = toSchemaValues(flag.schema);
|
|
1611
|
+
if (values) {
|
|
1612
|
+
const typed = partial.toLowerCase();
|
|
1613
|
+
return {
|
|
1614
|
+
candidates: values.filter((value) => value.toLowerCase().startsWith(typed)).map((value) => ({ value: `${prefix}${value}` })),
|
|
1615
|
+
isFile: false
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
return {
|
|
1619
|
+
candidates: [],
|
|
1620
|
+
isFile: PATH_VALUES.has(flag.valueName ?? "")
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
function toArgumentValues(walked, partial) {
|
|
1624
|
+
const declared = walked.command?.arguments ?? [];
|
|
1625
|
+
const last = declared[declared.length - 1];
|
|
1626
|
+
return ((walked.positionals < declared.length ? declared[walked.positionals] : last?.variadic ? last : void 0)?.values ?? []).filter((value) => value.startsWith(partial)).map((value) => ({ value }));
|
|
1627
|
+
}
|
|
1628
|
+
/** toSchemaValues reads the values a schema allows, when it allows only a fixed set. */
|
|
1629
|
+
function toSchemaValues(schema) {
|
|
1630
|
+
if (schema instanceof z.ZodEnum) return schema.options.map(String);
|
|
1631
|
+
if (schema instanceof z.ZodPipe) return toSchemaValues(schema.out);
|
|
1632
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) return toSchemaValues(schema.unwrap());
|
|
1633
|
+
}
|
|
1634
|
+
/** toLine keeps a description on one line, because a newline would start another candidate. */
|
|
1635
|
+
function toLine(description) {
|
|
1636
|
+
return description.replace(/\s+/g, " ").trim();
|
|
1637
|
+
}
|
|
1638
|
+
async function runCompletion(input) {
|
|
1639
|
+
const shell = input.args[0] ?? "";
|
|
1640
|
+
if (!SHELLS.includes(shell)) {
|
|
1641
|
+
writeFailure(`${shell || "no shell"} is not one this CLI writes for. Choose ${SHELLS.join(", ")}`, input.isJSON);
|
|
1642
|
+
return ExitCode.USAGE;
|
|
1643
|
+
}
|
|
1644
|
+
writeData(toScript(shell));
|
|
1645
|
+
return ExitCode.OK;
|
|
1646
|
+
}
|
|
1647
|
+
//#endregion
|
|
1154
1648
|
//#region src/command/config.ts
|
|
1155
1649
|
const configCommand = defineCommand({
|
|
1156
1650
|
name: "config",
|
|
@@ -1201,47 +1695,6 @@ async function runConfig(input) {
|
|
|
1201
1695
|
return ExitCode.OK;
|
|
1202
1696
|
}
|
|
1203
1697
|
//#endregion
|
|
1204
|
-
//#region src/auth/browser.ts
|
|
1205
|
-
/** isWSL reports whether this Linux is running under Windows. */
|
|
1206
|
-
function isWSL() {
|
|
1207
|
-
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
1208
|
-
}
|
|
1209
|
-
/**
|
|
1210
|
-
* toOpeners lists the ways to open a URL here, best first. WSL needs Windows to do it,
|
|
1211
|
-
* because the browser the person is looking at is the Windows one, and xdg-open reaches
|
|
1212
|
-
* nothing outside the distribution.
|
|
1213
|
-
*/
|
|
1214
|
-
function toOpeners(url) {
|
|
1215
|
-
if (process.platform === "darwin") return [["open", [url]]];
|
|
1216
|
-
if (process.platform === "win32") return [["cmd", [
|
|
1217
|
-
"/c",
|
|
1218
|
-
"start",
|
|
1219
|
-
"",
|
|
1220
|
-
url
|
|
1221
|
-
]]];
|
|
1222
|
-
if (isWSL()) return [
|
|
1223
|
-
["wslview", [url]],
|
|
1224
|
-
["powershell.exe", [
|
|
1225
|
-
"-NoProfile",
|
|
1226
|
-
"-NonInteractive",
|
|
1227
|
-
"-Command",
|
|
1228
|
-
"Start-Process",
|
|
1229
|
-
`'${url}'`
|
|
1230
|
-
]],
|
|
1231
|
-
["explorer.exe", [url]]
|
|
1232
|
-
];
|
|
1233
|
-
return [["xdg-open", [url]]];
|
|
1234
|
-
}
|
|
1235
|
-
/** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
|
|
1236
|
-
function openBrowser(url, launch = spawnSync) {
|
|
1237
|
-
for (const [command, args] of toOpeners(url)) if (launch(command, args, {
|
|
1238
|
-
stdio: "ignore",
|
|
1239
|
-
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
1240
|
-
timeout: 1e4
|
|
1241
|
-
}).error === void 0) return true;
|
|
1242
|
-
return false;
|
|
1243
|
-
}
|
|
1244
|
-
//#endregion
|
|
1245
1698
|
//#region src/auth/device.ts
|
|
1246
1699
|
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1247
1700
|
/** The interval a server gives is in seconds, and five more are added on a slow_down. */
|
|
@@ -1653,7 +2106,8 @@ async function toSignedIn(input, metadata, tokens) {
|
|
|
1653
2106
|
`Scope ${tokens.scope ?? "as granted"}`,
|
|
1654
2107
|
`Stored in ${stored}`,
|
|
1655
2108
|
"",
|
|
1656
|
-
"Run hardfin status to see what this CLI is using"
|
|
2109
|
+
"Run hardfin status to see what this CLI is using",
|
|
2110
|
+
...isInstalled() ? [] : ["Run hardfin completion install to complete commands with Tab"]
|
|
1657
2111
|
].join("\n"));
|
|
1658
2112
|
return ExitCode.OK;
|
|
1659
2113
|
}
|
|
@@ -1947,7 +2401,7 @@ function toCliRunner() {
|
|
|
1947
2401
|
}
|
|
1948
2402
|
/** serve answers requests on stdin until the client closes it. */
|
|
1949
2403
|
async function serve(commands, run = toCliRunner()) {
|
|
1950
|
-
const lines = createInterface({ input: process.stdin });
|
|
2404
|
+
const lines = createInterface$1({ input: process.stdin });
|
|
1951
2405
|
const answering = /* @__PURE__ */ new Set();
|
|
1952
2406
|
for await (const line of lines) {
|
|
1953
2407
|
if (line.trim() === "") continue;
|
|
@@ -5201,7 +5655,8 @@ function toRejectedFlag(command, flags) {
|
|
|
5201
5655
|
/** toCli builds the parser from the registry, which is what every surface reads. */
|
|
5202
5656
|
function toCli(entries = commands) {
|
|
5203
5657
|
const program = new Command();
|
|
5204
|
-
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").
|
|
5658
|
+
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").showHelpAfterError().enablePositionalOptions();
|
|
5659
|
+
for (const flag of GLOBAL_FLAGS) program.option(`--${flag.name} <${flag.valueName}>`, flag.description);
|
|
5205
5660
|
for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
|
|
5206
5661
|
return program;
|
|
5207
5662
|
}
|