@hardfin/cli 0.1.0-dev.23 → 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 +487 -237
- 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 = {
|
|
@@ -968,17 +969,469 @@ function toBody$1(source) {
|
|
|
968
969
|
}
|
|
969
970
|
}
|
|
970
971
|
//#endregion
|
|
971
|
-
//#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
|
|
972
1014
|
const SHELLS = [
|
|
973
1015
|
"bash",
|
|
974
1016
|
"zsh",
|
|
975
1017
|
"fish",
|
|
976
1018
|
"powershell"
|
|
977
1019
|
];
|
|
1020
|
+
/** toScript writes a shell's completion, each one asking __complete for the candidates. */
|
|
1021
|
+
function toScript(shell) {
|
|
1022
|
+
return SCRIPTS[shell];
|
|
1023
|
+
}
|
|
1024
|
+
const SCRIPTS = {
|
|
1025
|
+
bash: `# hardfin completion for bash
|
|
1026
|
+
_hardfin_words() {
|
|
1027
|
+
# Bash splits a word at = and :, so the pieces are joined again wherever no space
|
|
1028
|
+
# separates them in the line, and the word under the cursor ends at the cursor.
|
|
1029
|
+
local line=\${COMP_LINE:0:COMP_POINT} offset=0 i word rest gap
|
|
1030
|
+
_hardfin_line_words=()
|
|
1031
|
+
|
|
1032
|
+
for ((i = 0; i <= COMP_CWORD; i++)); do
|
|
1033
|
+
rest=\${line:offset}
|
|
1034
|
+
|
|
1035
|
+
if ((i < COMP_CWORD)); then
|
|
1036
|
+
word=\${COMP_WORDS[i]}
|
|
1037
|
+
gap=\${rest%%"$word"*}
|
|
1038
|
+
offset=$((offset + \${#gap} + \${#word}))
|
|
1039
|
+
else
|
|
1040
|
+
word=\${rest#"\${rest%%[![:space:]]*}"}
|
|
1041
|
+
gap=\${rest%"$word"}
|
|
1042
|
+
fi
|
|
1043
|
+
|
|
1044
|
+
if ((i > 0)) && [[ -z $gap ]]; then
|
|
1045
|
+
_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]+=$word
|
|
1046
|
+
else
|
|
1047
|
+
_hardfin_line_words+=("$word")
|
|
1048
|
+
fi
|
|
1049
|
+
done
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
_hardfin_complete() {
|
|
1053
|
+
local _hardfin_line_words line directive current prefix breaks
|
|
1054
|
+
_hardfin_words
|
|
1055
|
+
COMPREPLY=()
|
|
1056
|
+
|
|
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
|
|
1065
|
+
done < <(hardfin __complete -- "\${_hardfin_line_words[@]:1}" 2>/dev/null)
|
|
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
|
+
|
|
1075
|
+
# Readline replaces only the text after the last = or : in the word, so a candidate
|
|
1076
|
+
# loses what comes before it or the line would repeat that part.
|
|
1077
|
+
current=\${_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]}
|
|
1078
|
+
breaks=\${COMP_WORDBREAKS//[^=:]/}
|
|
1079
|
+
|
|
1080
|
+
if [[ -n $breaks && $current == *["$breaks"]* ]]; then
|
|
1081
|
+
prefix=\${current%"\${current##*["$breaks"]}"}
|
|
1082
|
+
COMPREPLY=("\${COMPREPLY[@]#"$prefix"}")
|
|
1083
|
+
fi
|
|
1084
|
+
}
|
|
1085
|
+
|
|
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`,
|
|
1092
|
+
zsh: `# hardfin completion for zsh
|
|
1093
|
+
_hardfin_complete() {
|
|
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}'
|
|
1120
|
+
}
|
|
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
|
+
|
|
1127
|
+
compdef _hardfin_complete hardfin`,
|
|
1128
|
+
fish: `# hardfin completion for fish
|
|
1129
|
+
function __hardfin_complete
|
|
1130
|
+
# Older fish lacks -x, which replaced -o, so it falls back to the older option.
|
|
1131
|
+
set -l words (commandline -xpc 2>/dev/null; or commandline -opc)
|
|
1132
|
+
set -l current (commandline -ct)
|
|
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
|
|
1148
|
+
end
|
|
1149
|
+
|
|
1150
|
+
complete -c hardfin -f -a "(__hardfin_complete)"`,
|
|
1151
|
+
powershell: `# hardfin completion for PowerShell
|
|
1152
|
+
Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
1153
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
1154
|
+
|
|
1155
|
+
# Only the words before the cursor decide what is offered, and PowerShell leaves out the
|
|
1156
|
+
# empty word after a trailing space, so it is added back.
|
|
1157
|
+
$words = @($commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_.Extent.StartOffset -lt $cursorPosition } | ForEach-Object { $_.Extent.Text })
|
|
1158
|
+
|
|
1159
|
+
if ($wordToComplete -eq '') {
|
|
1160
|
+
# Before 7.3, and in Legacy mode, an empty argument never reaches the program unless
|
|
1161
|
+
# it is written as a pair of quotes.
|
|
1162
|
+
$isLegacy = $PSVersionTable.PSVersion -lt [version]'7.3' -or $PSNativeCommandArgumentPassing -eq 'Legacy'
|
|
1163
|
+
$words += if ($isLegacy) { '""' } else { '' }
|
|
1164
|
+
}
|
|
1165
|
+
|
|
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
|
|
1178
|
+
|
|
1179
|
+
if ($text -match '[\\s#@$;,''{}()"\`|<>&]') {
|
|
1180
|
+
$text = "'" + ($text -replace "'", "''") + "'"
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
[System.Management.Automation.CompletionResult]::new($text, $value, 'ParameterValue', $(if ($description) { $description } else { $value }))
|
|
1184
|
+
}
|
|
1185
|
+
}`
|
|
1186
|
+
};
|
|
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
|
|
978
1431
|
const completionCommand = defineCommand({
|
|
979
1432
|
name: "completion",
|
|
980
|
-
summary: "
|
|
981
|
-
description: "
|
|
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.",
|
|
982
1435
|
arguments: [{
|
|
983
1436
|
name: "shell",
|
|
984
1437
|
description: `The shell to write for: ${SHELLS.join(", ")}`,
|
|
@@ -986,20 +1439,14 @@ const completionCommand = defineCommand({
|
|
|
986
1439
|
values: [...SHELLS]
|
|
987
1440
|
}],
|
|
988
1441
|
flags: [],
|
|
989
|
-
examples: [
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
},
|
|
998
|
-
{
|
|
999
|
-
description: "Complete in bash",
|
|
1000
|
-
command: "source <(hardfin completion bash)"
|
|
1001
|
-
}
|
|
1002
|
-
],
|
|
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],
|
|
1003
1450
|
run: runCompletion
|
|
1004
1451
|
});
|
|
1005
1452
|
/** The hidden command a completion script asks, which keeps one implementation for every shell. */
|
|
@@ -1076,15 +1523,23 @@ function toCompletion(commands, words) {
|
|
|
1076
1523
|
candidates: toFlagCandidates(walked).filter((candidate) => candidate.value.startsWith(partial)),
|
|
1077
1524
|
isFile: false
|
|
1078
1525
|
};
|
|
1079
|
-
const subcommands = walked.command
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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)],
|
|
1085
1536
|
isFile: false
|
|
1086
1537
|
};
|
|
1087
|
-
|
|
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 ?? [] : [];
|
|
1088
1543
|
}
|
|
1089
1544
|
function toWalked(commands, words) {
|
|
1090
1545
|
let walked = {
|
|
@@ -1108,7 +1563,7 @@ function toWalked(commands, words) {
|
|
|
1108
1563
|
if (flag?.valueName && !word.includes("=")) walked.pending = flag;
|
|
1109
1564
|
continue;
|
|
1110
1565
|
}
|
|
1111
|
-
const found = (
|
|
1566
|
+
const found = toSubcommands(commands, walked).find((entry) => entry.name === word);
|
|
1112
1567
|
if (found) {
|
|
1113
1568
|
walked = {
|
|
1114
1569
|
command: found,
|
|
@@ -1168,10 +1623,7 @@ function toValues(flag, partial, prefix) {
|
|
|
1168
1623
|
function toArgumentValues(walked, partial) {
|
|
1169
1624
|
const declared = walked.command?.arguments ?? [];
|
|
1170
1625
|
const last = declared[declared.length - 1];
|
|
1171
|
-
return {
|
|
1172
|
-
candidates: ((walked.positionals < declared.length ? declared[walked.positionals] : last?.variadic ? last : void 0)?.values ?? []).filter((value) => value.startsWith(partial)).map((value) => ({ value })),
|
|
1173
|
-
isFile: false
|
|
1174
|
-
};
|
|
1626
|
+
return ((walked.positionals < declared.length ? declared[walked.positionals] : last?.variadic ? last : void 0)?.values ?? []).filter((value) => value.startsWith(partial)).map((value) => ({ value }));
|
|
1175
1627
|
}
|
|
1176
1628
|
/** toSchemaValues reads the values a schema allows, when it allows only a fixed set. */
|
|
1177
1629
|
function toSchemaValues(schema) {
|
|
@@ -1192,168 +1644,6 @@ async function runCompletion(input) {
|
|
|
1192
1644
|
writeData(toScript(shell));
|
|
1193
1645
|
return ExitCode.OK;
|
|
1194
1646
|
}
|
|
1195
|
-
/** toScript writes a shell's completion, each one asking __complete for the candidates. */
|
|
1196
|
-
function toScript(shell) {
|
|
1197
|
-
return SCRIPTS[shell];
|
|
1198
|
-
}
|
|
1199
|
-
const SCRIPTS = {
|
|
1200
|
-
bash: `# hardfin completion for bash
|
|
1201
|
-
_hardfin_words() {
|
|
1202
|
-
# Bash splits a word at = and :, so the pieces are joined again wherever no space
|
|
1203
|
-
# separates them in the line, and the word under the cursor ends at the cursor.
|
|
1204
|
-
local line=\${COMP_LINE:0:COMP_POINT} offset=0 i word rest gap
|
|
1205
|
-
_hardfin_line_words=()
|
|
1206
|
-
|
|
1207
|
-
for ((i = 0; i <= COMP_CWORD; i++)); do
|
|
1208
|
-
rest=\${line:offset}
|
|
1209
|
-
|
|
1210
|
-
if ((i < COMP_CWORD)); then
|
|
1211
|
-
word=\${COMP_WORDS[i]}
|
|
1212
|
-
gap=\${rest%%"$word"*}
|
|
1213
|
-
offset=$((offset + \${#gap} + \${#word}))
|
|
1214
|
-
else
|
|
1215
|
-
word=\${rest#"\${rest%%[![:space:]]*}"}
|
|
1216
|
-
gap=\${rest%"$word"}
|
|
1217
|
-
fi
|
|
1218
|
-
|
|
1219
|
-
if ((i > 0)) && [[ -z $gap ]]; then
|
|
1220
|
-
_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]+=$word
|
|
1221
|
-
else
|
|
1222
|
-
_hardfin_line_words+=("$word")
|
|
1223
|
-
fi
|
|
1224
|
-
done
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
_hardfin_complete() {
|
|
1228
|
-
local _hardfin_line_words line directive current prefix breaks
|
|
1229
|
-
_hardfin_words
|
|
1230
|
-
COMPREPLY=()
|
|
1231
|
-
|
|
1232
|
-
# Each line is a candidate with its description after a tab, which bash cannot show, and
|
|
1233
|
-
# the last line is the directive.
|
|
1234
|
-
while IFS= read -r line; do
|
|
1235
|
-
if [[ $line == :* ]]; then
|
|
1236
|
-
directive=$line
|
|
1237
|
-
else
|
|
1238
|
-
COMPREPLY+=("\${line%%$'\\t'*}")
|
|
1239
|
-
fi
|
|
1240
|
-
done < <(hardfin __complete -- "\${_hardfin_line_words[@]:1}" 2>/dev/null)
|
|
1241
|
-
|
|
1242
|
-
# A path is left to readline, which completes it from the file system.
|
|
1243
|
-
if [[ $directive == :1 ]]; then
|
|
1244
|
-
compopt -o default 2>/dev/null
|
|
1245
|
-
COMPREPLY=()
|
|
1246
|
-
|
|
1247
|
-
return
|
|
1248
|
-
fi
|
|
1249
|
-
|
|
1250
|
-
# Readline replaces only the text after the last = or : in the word, so a candidate
|
|
1251
|
-
# loses what comes before it or the line would repeat that part.
|
|
1252
|
-
current=\${_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]}
|
|
1253
|
-
breaks=\${COMP_WORDBREAKS//[^=:]/}
|
|
1254
|
-
|
|
1255
|
-
if [[ -n $breaks && $current == *["$breaks"]* ]]; then
|
|
1256
|
-
prefix=\${current%"\${current##*["$breaks"]}"}
|
|
1257
|
-
COMPREPLY=("\${COMPREPLY[@]#"$prefix"}")
|
|
1258
|
-
fi
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
# Bash before 4.0 has no compopt, so files are offered whenever no candidate is.
|
|
1262
|
-
if type compopt >/dev/null 2>&1; then
|
|
1263
|
-
complete -F _hardfin_complete hardfin
|
|
1264
|
-
else
|
|
1265
|
-
complete -o default -F _hardfin_complete hardfin
|
|
1266
|
-
fi`,
|
|
1267
|
-
zsh: `# hardfin completion for zsh
|
|
1268
|
-
_hardfin_complete() {
|
|
1269
|
-
local -a lines described
|
|
1270
|
-
local line name directive
|
|
1271
|
-
lines=(\${(f)"$(hardfin __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)"})
|
|
1272
|
-
directive=\${lines[-1]}
|
|
1273
|
-
lines=(\${lines[1,-2]})
|
|
1274
|
-
|
|
1275
|
-
if [[ $directive == :1 ]]; then
|
|
1276
|
-
_files
|
|
1277
|
-
|
|
1278
|
-
return
|
|
1279
|
-
fi
|
|
1280
|
-
|
|
1281
|
-
# Each line is a candidate with its description after a tab, and _describe separates the
|
|
1282
|
-
# two with a colon, so a colon in the candidate is escaped.
|
|
1283
|
-
for line in $lines; do
|
|
1284
|
-
name=\${line%%$'\\t'*}
|
|
1285
|
-
name=\${name//:/\\\\:}
|
|
1286
|
-
|
|
1287
|
-
if [[ $line == *$'\\t'* ]]; then
|
|
1288
|
-
described+=("$name:\${line#*$'\\t'}")
|
|
1289
|
-
else
|
|
1290
|
-
described+=("$name")
|
|
1291
|
-
fi
|
|
1292
|
-
done
|
|
1293
|
-
|
|
1294
|
-
_describe 'hardfin' described -M 'm:{a-zA-Z}={A-Za-z}'
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
compdef _hardfin_complete hardfin`,
|
|
1298
|
-
fish: `# hardfin completion for fish
|
|
1299
|
-
function __hardfin_complete
|
|
1300
|
-
# Older fish lacks -x, which replaced -o, so it falls back to the older option.
|
|
1301
|
-
set -l words (commandline -xpc 2>/dev/null; or commandline -opc)
|
|
1302
|
-
set -l current (commandline -ct)
|
|
1303
|
-
set -l lines (hardfin __complete -- $words[2..-1] "$current" 2>/dev/null)
|
|
1304
|
-
set -l directive $lines[-1]
|
|
1305
|
-
|
|
1306
|
-
if set -q lines[1]
|
|
1307
|
-
set -e lines[-1]
|
|
1308
|
-
end
|
|
1309
|
-
|
|
1310
|
-
if test "$directive" = ":1"
|
|
1311
|
-
__fish_complete_path "$current"
|
|
1312
|
-
|
|
1313
|
-
return
|
|
1314
|
-
end
|
|
1315
|
-
|
|
1316
|
-
# Fish shows what follows a tab as the description, so candidates pass through as they are.
|
|
1317
|
-
string join \\n -- $lines
|
|
1318
|
-
end
|
|
1319
|
-
|
|
1320
|
-
complete -c hardfin -f -a "(__hardfin_complete)"`,
|
|
1321
|
-
powershell: `# hardfin completion for PowerShell
|
|
1322
|
-
Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
1323
|
-
param($wordToComplete, $commandAst, $cursorPosition)
|
|
1324
|
-
|
|
1325
|
-
# Only the words before the cursor decide what is offered, and PowerShell leaves out the
|
|
1326
|
-
# empty word after a trailing space, so it is added back.
|
|
1327
|
-
$words = @($commandAst.CommandElements | Select-Object -Skip 1 | Where-Object { $_.Extent.StartOffset -lt $cursorPosition } | ForEach-Object { $_.Extent.Text })
|
|
1328
|
-
|
|
1329
|
-
if ($wordToComplete -eq '') {
|
|
1330
|
-
# Before 7.3, and in Legacy mode, an empty argument never reaches the program unless
|
|
1331
|
-
# it is written as a pair of quotes.
|
|
1332
|
-
$isLegacy = $PSVersionTable.PSVersion -lt [version]'7.3' -or $PSNativeCommandArgumentPassing -eq 'Legacy'
|
|
1333
|
-
$words += if ($isLegacy) { '""' } else { '' }
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
$lines = @(hardfin __complete -- @words 2>$null)
|
|
1337
|
-
|
|
1338
|
-
# Returning nothing makes PowerShell complete paths, which is what the last line asks for
|
|
1339
|
-
# when it is :1.
|
|
1340
|
-
if ($lines.Count -eq 0 -or $lines[-1] -eq ':1') {
|
|
1341
|
-
return
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
# Each line is a candidate with its description after a tab.
|
|
1345
|
-
$lines | Select-Object -SkipLast 1 | ForEach-Object {
|
|
1346
|
-
$value, $description = $_ -split "\`t", 2
|
|
1347
|
-
$text = $value
|
|
1348
|
-
|
|
1349
|
-
if ($text -match '[\\s#@$;,''{}()"\`|<>&]') {
|
|
1350
|
-
$text = "'" + ($text -replace "'", "''") + "'"
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
[System.Management.Automation.CompletionResult]::new($text, $value, 'ParameterValue', $(if ($description) { $description } else { $value }))
|
|
1354
|
-
}
|
|
1355
|
-
}`
|
|
1356
|
-
};
|
|
1357
1647
|
//#endregion
|
|
1358
1648
|
//#region src/command/config.ts
|
|
1359
1649
|
const configCommand = defineCommand({
|
|
@@ -1405,47 +1695,6 @@ async function runConfig(input) {
|
|
|
1405
1695
|
return ExitCode.OK;
|
|
1406
1696
|
}
|
|
1407
1697
|
//#endregion
|
|
1408
|
-
//#region src/auth/browser.ts
|
|
1409
|
-
/** isWSL reports whether this Linux is running under Windows. */
|
|
1410
|
-
function isWSL() {
|
|
1411
|
-
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
1412
|
-
}
|
|
1413
|
-
/**
|
|
1414
|
-
* toOpeners lists the ways to open a URL here, best first. WSL needs Windows to do it,
|
|
1415
|
-
* because the browser the person is looking at is the Windows one, and xdg-open reaches
|
|
1416
|
-
* nothing outside the distribution.
|
|
1417
|
-
*/
|
|
1418
|
-
function toOpeners(url) {
|
|
1419
|
-
if (process.platform === "darwin") return [["open", [url]]];
|
|
1420
|
-
if (process.platform === "win32") return [["cmd", [
|
|
1421
|
-
"/c",
|
|
1422
|
-
"start",
|
|
1423
|
-
"",
|
|
1424
|
-
url
|
|
1425
|
-
]]];
|
|
1426
|
-
if (isWSL()) return [
|
|
1427
|
-
["wslview", [url]],
|
|
1428
|
-
["powershell.exe", [
|
|
1429
|
-
"-NoProfile",
|
|
1430
|
-
"-NonInteractive",
|
|
1431
|
-
"-Command",
|
|
1432
|
-
"Start-Process",
|
|
1433
|
-
`'${url}'`
|
|
1434
|
-
]],
|
|
1435
|
-
["explorer.exe", [url]]
|
|
1436
|
-
];
|
|
1437
|
-
return [["xdg-open", [url]]];
|
|
1438
|
-
}
|
|
1439
|
-
/** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
|
|
1440
|
-
function openBrowser(url, launch = spawnSync) {
|
|
1441
|
-
for (const [command, args] of toOpeners(url)) if (launch(command, args, {
|
|
1442
|
-
stdio: "ignore",
|
|
1443
|
-
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
1444
|
-
timeout: 1e4
|
|
1445
|
-
}).error === void 0) return true;
|
|
1446
|
-
return false;
|
|
1447
|
-
}
|
|
1448
|
-
//#endregion
|
|
1449
1698
|
//#region src/auth/device.ts
|
|
1450
1699
|
const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1451
1700
|
/** The interval a server gives is in seconds, and five more are added on a slow_down. */
|
|
@@ -1857,7 +2106,8 @@ async function toSignedIn(input, metadata, tokens) {
|
|
|
1857
2106
|
`Scope ${tokens.scope ?? "as granted"}`,
|
|
1858
2107
|
`Stored in ${stored}`,
|
|
1859
2108
|
"",
|
|
1860
|
-
"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"]
|
|
1861
2111
|
].join("\n"));
|
|
1862
2112
|
return ExitCode.OK;
|
|
1863
2113
|
}
|
|
@@ -2151,7 +2401,7 @@ function toCliRunner() {
|
|
|
2151
2401
|
}
|
|
2152
2402
|
/** serve answers requests on stdin until the client closes it. */
|
|
2153
2403
|
async function serve(commands, run = toCliRunner()) {
|
|
2154
|
-
const lines = createInterface({ input: process.stdin });
|
|
2404
|
+
const lines = createInterface$1({ input: process.stdin });
|
|
2155
2405
|
const answering = /* @__PURE__ */ new Set();
|
|
2156
2406
|
for await (const line of lines) {
|
|
2157
2407
|
if (line.trim() === "") continue;
|