@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.
Files changed (2) hide show
  1. package/dist/cli.js +239 -34
  2. 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(toCandidates(input.commands, input.args).join("\n"));
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
- * 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.
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 toCandidates(commands, words) {
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 (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));
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 remaining = commands;
1027
- let command;
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 (word.startsWith("-") || command?.arguments.length) continue;
1030
- const found = remaining.find((entry) => entry.name === word);
1031
- if (!found) return {
1032
- command,
1033
- remaining,
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
- command = found;
1037
- remaining = found.subcommands ?? [];
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
- command,
1041
- remaining,
1042
- isUnknown: false
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
- function toFlagNames(command) {
1046
- return [...(command?.flags ?? []).map((flag) => `--${flag.name}`), "--help"];
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 candidate current prefix breaks
1228
+ local _hardfin_line_words line directive current prefix breaks
1091
1229
  _hardfin_words
1092
1230
  COMPREPLY=()
1093
1231
 
1094
- while IFS= read -r candidate; do
1095
- COMPREPLY+=("$candidate")
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
- complete -F _hardfin_complete hardfin`,
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 candidates
1113
- candidates=(\${(f)"$(hardfin __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)"})
1114
- compadd -a candidates
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 | ForEach-Object {
1143
- $text = $_
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, $_, 'ParameterValue', $_)
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").option("--api-url <url>", "The API to call, whose host also holds the authorization server").option("--issuer-url <url>", "The authorization server, when it does not sit at the API's host").showHelpAfterError().enablePositionalOptions();
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.1.0-dev.22",
3
+ "version": "0.1.0-dev.23",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",