@hardfin/cli 0.1.0-dev.22 → 0.1.0-dev.23
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/dist/cli.js +239 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,18 @@ const ExitCode = {
|
|
|
21
21
|
function defineCommand(command) {
|
|
22
22
|
return command;
|
|
23
23
|
}
|
|
24
|
+
/** GLOBAL_FLAGS are the options the CLI takes before any command. */
|
|
25
|
+
const GLOBAL_FLAGS = [{
|
|
26
|
+
name: "api-url",
|
|
27
|
+
description: "The API to call, whose host also holds the authorization server",
|
|
28
|
+
valueName: "url",
|
|
29
|
+
schema: z.string()
|
|
30
|
+
}, {
|
|
31
|
+
name: "issuer-url",
|
|
32
|
+
description: "The authorization server, when it does not sit at the API's host",
|
|
33
|
+
valueName: "url",
|
|
34
|
+
schema: z.string()
|
|
35
|
+
}];
|
|
24
36
|
//#endregion
|
|
25
37
|
//#region src/config/settings.ts
|
|
26
38
|
/** The API version this build was written against, sent on every request. */
|
|
@@ -970,7 +982,8 @@ const completionCommand = defineCommand({
|
|
|
970
982
|
arguments: [{
|
|
971
983
|
name: "shell",
|
|
972
984
|
description: `The shell to write for: ${SHELLS.join(", ")}`,
|
|
973
|
-
required: true
|
|
985
|
+
required: true,
|
|
986
|
+
values: [...SHELLS]
|
|
974
987
|
}],
|
|
975
988
|
flags: [],
|
|
976
989
|
examples: [
|
|
@@ -1007,43 +1020,168 @@ const completeCommand = defineCommand({
|
|
|
1007
1020
|
}],
|
|
1008
1021
|
examples: [],
|
|
1009
1022
|
run: async (input) => {
|
|
1010
|
-
writeData(
|
|
1023
|
+
writeData(toAnswer$1(toCompletion(input.commands, input.args)));
|
|
1011
1024
|
return ExitCode.OK;
|
|
1012
1025
|
}
|
|
1013
1026
|
});
|
|
1027
|
+
/** Directive is the last line of an answer, which tells the script what to do beyond the candidates. */
|
|
1028
|
+
const Directive = {
|
|
1029
|
+
NONE: ":0",
|
|
1030
|
+
FILES: ":1"
|
|
1031
|
+
};
|
|
1032
|
+
const HELP_FLAG = {
|
|
1033
|
+
name: "help",
|
|
1034
|
+
short: "h",
|
|
1035
|
+
description: "Print help for the command",
|
|
1036
|
+
schema: z.boolean()
|
|
1037
|
+
};
|
|
1038
|
+
const VERSION_FLAG = {
|
|
1039
|
+
name: "version",
|
|
1040
|
+
short: "v",
|
|
1041
|
+
description: "Print the version",
|
|
1042
|
+
schema: z.boolean()
|
|
1043
|
+
};
|
|
1044
|
+
/** The value names that mean a path, so the shell offers files for them. */
|
|
1045
|
+
const PATH_VALUES = /* @__PURE__ */ new Set(["path", "file"]);
|
|
1046
|
+
const NOTHING = {
|
|
1047
|
+
candidates: [],
|
|
1048
|
+
isFile: false
|
|
1049
|
+
};
|
|
1050
|
+
/**
|
|
1051
|
+
* toAnswer writes a completion as a script reads it. Each line is a candidate, with its
|
|
1052
|
+
* description after a tab, and the last line is the directive.
|
|
1053
|
+
*/
|
|
1054
|
+
function toAnswer$1(completion) {
|
|
1055
|
+
return [...completion.candidates.map((candidate) => candidate.description ? `${candidate.value}\t${toLine(candidate.description)}` : candidate.value), completion.isFile ? Directive.FILES : Directive.NONE].join("\n");
|
|
1056
|
+
}
|
|
1014
1057
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1058
|
+
* toCompletion answers what may follow the words typed so far. The last word is what is
|
|
1059
|
+
* being typed, and it is empty when the cursor sits after a space, which asks what may come
|
|
1060
|
+
* next rather than how to finish the word before it.
|
|
1017
1061
|
*/
|
|
1018
|
-
function
|
|
1062
|
+
function toCompletion(commands, words) {
|
|
1019
1063
|
const partial = words[words.length - 1] ?? "";
|
|
1020
1064
|
const walked = toWalked(commands, words.slice(0, -1));
|
|
1021
|
-
if (
|
|
1022
|
-
if (walked.
|
|
1023
|
-
|
|
1065
|
+
if (walked.isUnknown) return NOTHING;
|
|
1066
|
+
if (walked.pending) return toValues(walked.pending, partial, "");
|
|
1067
|
+
const assigned = /^(--?[^=]+)=(.*)$/.exec(partial);
|
|
1068
|
+
if (assigned?.[1] !== void 0 && assigned[2] !== void 0) {
|
|
1069
|
+
const flag = toFlag(walked.flags, assigned[1]);
|
|
1070
|
+
return flag?.valueName ? {
|
|
1071
|
+
candidates: toValues(flag, assigned[2], `${assigned[1]}=`).candidates,
|
|
1072
|
+
isFile: false
|
|
1073
|
+
} : NOTHING;
|
|
1074
|
+
}
|
|
1075
|
+
if (partial.startsWith("-")) return {
|
|
1076
|
+
candidates: toFlagCandidates(walked).filter((candidate) => candidate.value.startsWith(partial)),
|
|
1077
|
+
isFile: false
|
|
1078
|
+
};
|
|
1079
|
+
const subcommands = walked.command ? walked.command.subcommands ?? [] : commands;
|
|
1080
|
+
if (subcommands.length > 0) return {
|
|
1081
|
+
candidates: subcommands.filter((command) => !command.hidden && command.name.startsWith(partial)).map((command) => ({
|
|
1082
|
+
value: command.name,
|
|
1083
|
+
description: command.summary
|
|
1084
|
+
})),
|
|
1085
|
+
isFile: false
|
|
1086
|
+
};
|
|
1087
|
+
return toArgumentValues(walked, partial);
|
|
1024
1088
|
}
|
|
1025
1089
|
function toWalked(commands, words) {
|
|
1026
|
-
let
|
|
1027
|
-
|
|
1090
|
+
let walked = {
|
|
1091
|
+
flags: [
|
|
1092
|
+
...GLOBAL_FLAGS,
|
|
1093
|
+
VERSION_FLAG,
|
|
1094
|
+
HELP_FLAG
|
|
1095
|
+
],
|
|
1096
|
+
used: /* @__PURE__ */ new Set(),
|
|
1097
|
+
positionals: 0,
|
|
1098
|
+
isUnknown: false
|
|
1099
|
+
};
|
|
1028
1100
|
for (const word of words) {
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1101
|
+
if (walked.pending) {
|
|
1102
|
+
walked.pending = void 0;
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (word.startsWith("-")) {
|
|
1106
|
+
const flag = toFlag(walked.flags, word.split("=")[0] ?? word);
|
|
1107
|
+
if (flag) walked.used.add(flag.name);
|
|
1108
|
+
if (flag?.valueName && !word.includes("=")) walked.pending = flag;
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
const found = (walked.command ? walked.command.subcommands ?? [] : commands).find((entry) => entry.name === word);
|
|
1112
|
+
if (found) {
|
|
1113
|
+
walked = {
|
|
1114
|
+
command: found,
|
|
1115
|
+
flags: [...found.flags, HELP_FLAG],
|
|
1116
|
+
used: /* @__PURE__ */ new Set(),
|
|
1117
|
+
positionals: 0,
|
|
1118
|
+
isUnknown: false
|
|
1119
|
+
};
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
if (walked.command?.arguments.length) {
|
|
1123
|
+
walked.positionals += 1;
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
...walked,
|
|
1034
1128
|
isUnknown: true
|
|
1035
1129
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1130
|
+
}
|
|
1131
|
+
return walked;
|
|
1132
|
+
}
|
|
1133
|
+
/** toFlag finds the flag a word names, whether long, short, or the --no- form of a boolean. */
|
|
1134
|
+
function toFlag(flags, word) {
|
|
1135
|
+
if (word.startsWith("--")) {
|
|
1136
|
+
const name = word.slice(2);
|
|
1137
|
+
return flags.find((flag) => flag.name === name || flag.negatable && `no-${flag.name}` === name);
|
|
1138
|
+
}
|
|
1139
|
+
return flags.find((flag) => flag.short !== void 0 && `-${flag.short}` === word);
|
|
1140
|
+
}
|
|
1141
|
+
function toFlagCandidates(walked) {
|
|
1142
|
+
return walked.flags.filter((flag) => flag.repeatable || !walked.used.has(flag.name)).flatMap((flag) => {
|
|
1143
|
+
const candidate = {
|
|
1144
|
+
value: `--${flag.name}`,
|
|
1145
|
+
description: flag.description
|
|
1146
|
+
};
|
|
1147
|
+
return flag.negatable ? [candidate, {
|
|
1148
|
+
value: `--no-${flag.name}`,
|
|
1149
|
+
description: `${flag.description}, turned off`
|
|
1150
|
+
}] : [candidate];
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
/** toValues offers the values a flag takes, each written after the prefix the word already has. */
|
|
1154
|
+
function toValues(flag, partial, prefix) {
|
|
1155
|
+
const values = toSchemaValues(flag.schema);
|
|
1156
|
+
if (values) {
|
|
1157
|
+
const typed = partial.toLowerCase();
|
|
1158
|
+
return {
|
|
1159
|
+
candidates: values.filter((value) => value.toLowerCase().startsWith(typed)).map((value) => ({ value: `${prefix}${value}` })),
|
|
1160
|
+
isFile: false
|
|
1161
|
+
};
|
|
1038
1162
|
}
|
|
1039
1163
|
return {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1164
|
+
candidates: [],
|
|
1165
|
+
isFile: PATH_VALUES.has(flag.valueName ?? "")
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function toArgumentValues(walked, partial) {
|
|
1169
|
+
const declared = walked.command?.arguments ?? [];
|
|
1170
|
+
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
|
|
1043
1174
|
};
|
|
1044
1175
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1176
|
+
/** toSchemaValues reads the values a schema allows, when it allows only a fixed set. */
|
|
1177
|
+
function toSchemaValues(schema) {
|
|
1178
|
+
if (schema instanceof z.ZodEnum) return schema.options.map(String);
|
|
1179
|
+
if (schema instanceof z.ZodPipe) return toSchemaValues(schema.out);
|
|
1180
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) return toSchemaValues(schema.unwrap());
|
|
1181
|
+
}
|
|
1182
|
+
/** toLine keeps a description on one line, because a newline would start another candidate. */
|
|
1183
|
+
function toLine(description) {
|
|
1184
|
+
return description.replace(/\s+/g, " ").trim();
|
|
1047
1185
|
}
|
|
1048
1186
|
async function runCompletion(input) {
|
|
1049
1187
|
const shell = input.args[0] ?? "";
|
|
@@ -1087,14 +1225,28 @@ _hardfin_words() {
|
|
|
1087
1225
|
}
|
|
1088
1226
|
|
|
1089
1227
|
_hardfin_complete() {
|
|
1090
|
-
local _hardfin_line_words
|
|
1228
|
+
local _hardfin_line_words line directive current prefix breaks
|
|
1091
1229
|
_hardfin_words
|
|
1092
1230
|
COMPREPLY=()
|
|
1093
1231
|
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
|
1096
1240
|
done < <(hardfin __complete -- "\${_hardfin_line_words[@]:1}" 2>/dev/null)
|
|
1097
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
|
+
|
|
1098
1250
|
# Readline replaces only the text after the last = or : in the word, so a candidate
|
|
1099
1251
|
# loses what comes before it or the line would repeat that part.
|
|
1100
1252
|
current=\${_hardfin_line_words[\${#_hardfin_line_words[@]} - 1]}
|
|
@@ -1106,12 +1258,40 @@ _hardfin_complete() {
|
|
|
1106
1258
|
fi
|
|
1107
1259
|
}
|
|
1108
1260
|
|
|
1109
|
-
|
|
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`,
|
|
1110
1267
|
zsh: `# hardfin completion for zsh
|
|
1111
1268
|
_hardfin_complete() {
|
|
1112
|
-
local -a
|
|
1113
|
-
|
|
1114
|
-
|
|
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}'
|
|
1115
1295
|
}
|
|
1116
1296
|
|
|
1117
1297
|
compdef _hardfin_complete hardfin`,
|
|
@@ -1120,7 +1300,21 @@ function __hardfin_complete
|
|
|
1120
1300
|
# Older fish lacks -x, which replaced -o, so it falls back to the older option.
|
|
1121
1301
|
set -l words (commandline -xpc 2>/dev/null; or commandline -opc)
|
|
1122
1302
|
set -l current (commandline -ct)
|
|
1123
|
-
hardfin __complete -- $words[2..-1] "$current" 2>/dev/null
|
|
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
|
|
1124
1318
|
end
|
|
1125
1319
|
|
|
1126
1320
|
complete -c hardfin -f -a "(__hardfin_complete)"`,
|
|
@@ -1139,14 +1333,24 @@ Register-ArgumentCompleter -Native -CommandName hardfin -ScriptBlock {
|
|
|
1139
1333
|
$words += if ($isLegacy) { '""' } else { '' }
|
|
1140
1334
|
}
|
|
1141
1335
|
|
|
1142
|
-
hardfin __complete -- @words 2>$null
|
|
1143
|
-
|
|
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
|
|
1144
1348
|
|
|
1145
1349
|
if ($text -match '[\\s#@$;,''{}()"\`|<>&]') {
|
|
1146
1350
|
$text = "'" + ($text -replace "'", "''") + "'"
|
|
1147
1351
|
}
|
|
1148
1352
|
|
|
1149
|
-
[System.Management.Automation.CompletionResult]::new($text, $
|
|
1353
|
+
[System.Management.Automation.CompletionResult]::new($text, $value, 'ParameterValue', $(if ($description) { $description } else { $value }))
|
|
1150
1354
|
}
|
|
1151
1355
|
}`
|
|
1152
1356
|
};
|
|
@@ -5201,7 +5405,8 @@ function toRejectedFlag(command, flags) {
|
|
|
5201
5405
|
/** toCli builds the parser from the registry, which is what every surface reads. */
|
|
5202
5406
|
function toCli(entries = commands) {
|
|
5203
5407
|
const program = new Command();
|
|
5204
|
-
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").
|
|
5408
|
+
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").showHelpAfterError().enablePositionalOptions();
|
|
5409
|
+
for (const flag of GLOBAL_FLAGS) program.option(`--${flag.name} <${flag.valueName}>`, flag.description);
|
|
5205
5410
|
for (const command of entries) program.addCommand(toProgram(program, command), { hidden: command.hidden });
|
|
5206
5411
|
return program;
|
|
5207
5412
|
}
|