@hardfin/cli 0.1.0-dev.24 → 0.1.0-dev.26
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 +1 -1
- package/dist/cli.js +15 -5720
- package/dist/completion-quick-BJHDp8OL.js +20 -0
- package/dist/completion-tree-rR_Rmeij.js +160 -0
- package/dist/completion-tree.json +2570 -0
- package/dist/program-C_9qi3K1.js +5704 -0
- package/package.json +2 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as toCompletion, t as toAnswer } from "./completion-tree-rR_Rmeij.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
//#region src/command/completion-quick.ts
|
|
5
|
+
/**
|
|
6
|
+
* toQuickAnswer answers a Tab from the tree the build wrote beside the CLI. It answers
|
|
7
|
+
* nothing when there is no tree, as in a test or a build made without one, and the caller
|
|
8
|
+
* then loads the commands instead.
|
|
9
|
+
*/
|
|
10
|
+
function toQuickAnswer(words, location = new URL("./completion-tree.json", import.meta.url)) {
|
|
11
|
+
let tree;
|
|
12
|
+
try {
|
|
13
|
+
tree = JSON.parse(readFileSync(location, "utf8"));
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return toAnswer(toCompletion(tree, words));
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { toQuickAnswer };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region src/command/completion-tree.ts
|
|
3
|
+
/** Directive is the last line of an answer, which tells the script what to do beyond the candidates. */
|
|
4
|
+
const Directive = {
|
|
5
|
+
NONE: ":0",
|
|
6
|
+
FILES: ":1"
|
|
7
|
+
};
|
|
8
|
+
const HELP_FLAG = {
|
|
9
|
+
name: "help",
|
|
10
|
+
short: "h",
|
|
11
|
+
description: "Print help for the command"
|
|
12
|
+
};
|
|
13
|
+
const VERSION_FLAG = {
|
|
14
|
+
name: "version",
|
|
15
|
+
short: "v",
|
|
16
|
+
description: "Print the version"
|
|
17
|
+
};
|
|
18
|
+
/** The value names that mean a path, so the shell offers files for them. */
|
|
19
|
+
const PATH_VALUES = /* @__PURE__ */ new Set(["path", "file"]);
|
|
20
|
+
const NOTHING = {
|
|
21
|
+
candidates: [],
|
|
22
|
+
isFile: false
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* toAnswer writes a completion as a script reads it. Each line is a candidate, with its
|
|
26
|
+
* description after a tab, and the last line is the directive.
|
|
27
|
+
*/
|
|
28
|
+
function toAnswer(completion) {
|
|
29
|
+
return [...completion.candidates.map((candidate) => candidate.description ? `${candidate.value}\t${toLine(candidate.description)}` : candidate.value), completion.isFile ? Directive.FILES : Directive.NONE].join("\n");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* toCompletion answers what may follow the words typed so far. The last word is what is
|
|
33
|
+
* being typed, and it is empty when the cursor sits after a space, which asks what may come
|
|
34
|
+
* next rather than how to finish the word before it.
|
|
35
|
+
*/
|
|
36
|
+
function toCompletion(tree, words) {
|
|
37
|
+
const partial = words[words.length - 1] ?? "";
|
|
38
|
+
const walked = toWalked(tree, words.slice(0, -1));
|
|
39
|
+
if (walked.isUnknown) return NOTHING;
|
|
40
|
+
if (walked.pending) return toValues(walked.pending, partial, "");
|
|
41
|
+
const assigned = /^(--?[^=]+)=(.*)$/.exec(partial);
|
|
42
|
+
if (assigned?.[1] !== void 0 && assigned[2] !== void 0) {
|
|
43
|
+
const flag = toFlag(walked.flags, assigned[1]);
|
|
44
|
+
return flag?.valueName ? {
|
|
45
|
+
candidates: toValues(flag, assigned[2], `${assigned[1]}=`).candidates,
|
|
46
|
+
isFile: false
|
|
47
|
+
} : NOTHING;
|
|
48
|
+
}
|
|
49
|
+
if (partial.startsWith("-")) return {
|
|
50
|
+
candidates: toFlagCandidates(walked).filter((candidate) => candidate.value.startsWith(partial)),
|
|
51
|
+
isFile: false
|
|
52
|
+
};
|
|
53
|
+
const subcommands = toSubcommands(tree.commands, walked).filter((command) => !command.hidden && command.name.startsWith(partial)).map((command) => ({
|
|
54
|
+
value: command.name,
|
|
55
|
+
description: command.summary
|
|
56
|
+
}));
|
|
57
|
+
if (walked.command && walked.command.subcommands.length > 0 && !walked.command.runs) return {
|
|
58
|
+
candidates: subcommands,
|
|
59
|
+
isFile: false
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
candidates: [...subcommands, ...toArgumentValues(walked, partial)],
|
|
63
|
+
isFile: false
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** toSubcommands lists the commands the next word may name. */
|
|
67
|
+
function toSubcommands(commands, walked) {
|
|
68
|
+
if (!walked.command) return commands;
|
|
69
|
+
return walked.positionals === 0 ? walked.command.subcommands : [];
|
|
70
|
+
}
|
|
71
|
+
function toWalked(tree, words) {
|
|
72
|
+
let walked = {
|
|
73
|
+
flags: [
|
|
74
|
+
...tree.globalFlags,
|
|
75
|
+
VERSION_FLAG,
|
|
76
|
+
HELP_FLAG
|
|
77
|
+
],
|
|
78
|
+
used: /* @__PURE__ */ new Set(),
|
|
79
|
+
positionals: 0,
|
|
80
|
+
isUnknown: false
|
|
81
|
+
};
|
|
82
|
+
for (const word of words) {
|
|
83
|
+
if (walked.pending) {
|
|
84
|
+
walked.pending = void 0;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (word.startsWith("-")) {
|
|
88
|
+
const flag = toFlag(walked.flags, word.split("=")[0] ?? word);
|
|
89
|
+
if (flag) walked.used.add(flag.name);
|
|
90
|
+
if (flag?.valueName && !word.includes("=")) walked.pending = flag;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const found = toSubcommands(tree.commands, walked).find((entry) => entry.name === word);
|
|
94
|
+
if (found) {
|
|
95
|
+
walked = {
|
|
96
|
+
command: found,
|
|
97
|
+
flags: [...found.flags, HELP_FLAG],
|
|
98
|
+
used: /* @__PURE__ */ new Set(),
|
|
99
|
+
positionals: 0,
|
|
100
|
+
isUnknown: false
|
|
101
|
+
};
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (walked.command?.arguments.length) {
|
|
105
|
+
walked.positionals += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...walked,
|
|
110
|
+
isUnknown: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return walked;
|
|
114
|
+
}
|
|
115
|
+
/** toFlag finds the flag a word names, whether long, short, or the --no- form of a boolean. */
|
|
116
|
+
function toFlag(flags, word) {
|
|
117
|
+
if (word.startsWith("--")) {
|
|
118
|
+
const name = word.slice(2);
|
|
119
|
+
return flags.find((flag) => flag.name === name || flag.negatable && `no-${flag.name}` === name);
|
|
120
|
+
}
|
|
121
|
+
return flags.find((flag) => flag.short !== void 0 && `-${flag.short}` === word);
|
|
122
|
+
}
|
|
123
|
+
function toFlagCandidates(walked) {
|
|
124
|
+
return walked.flags.filter((flag) => flag.repeatable || !walked.used.has(flag.name)).flatMap((flag) => {
|
|
125
|
+
const candidate = {
|
|
126
|
+
value: `--${flag.name}`,
|
|
127
|
+
description: flag.description
|
|
128
|
+
};
|
|
129
|
+
return flag.negatable ? [candidate, {
|
|
130
|
+
value: `--no-${flag.name}`,
|
|
131
|
+
description: `${flag.description}, turned off`
|
|
132
|
+
}] : [candidate];
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** toValues offers the values a flag takes, each written after the prefix the word already has. */
|
|
136
|
+
function toValues(flag, partial, prefix) {
|
|
137
|
+
const values = flag.values;
|
|
138
|
+
if (values) {
|
|
139
|
+
const typed = partial.toLowerCase();
|
|
140
|
+
return {
|
|
141
|
+
candidates: values.filter((value) => value.toLowerCase().startsWith(typed)).map((value) => ({ value: `${prefix}${value}` })),
|
|
142
|
+
isFile: false
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
candidates: [],
|
|
147
|
+
isFile: PATH_VALUES.has(flag.valueName ?? "")
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function toArgumentValues(walked, partial) {
|
|
151
|
+
const declared = walked.command?.arguments ?? [];
|
|
152
|
+
const last = declared[declared.length - 1];
|
|
153
|
+
return ((walked.positionals < declared.length ? declared[walked.positionals] : last?.variadic ? last : void 0)?.values ?? []).filter((value) => value.startsWith(partial)).map((value) => ({ value }));
|
|
154
|
+
}
|
|
155
|
+
/** toLine keeps a description on one line, because a newline would start another candidate. */
|
|
156
|
+
function toLine(description) {
|
|
157
|
+
return description.replace(/\s+/g, " ").trim();
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
export { toCompletion as n, toAnswer as t };
|