@crustjs/plugins 0.0.2

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 ADDED
@@ -0,0 +1,43 @@
1
+ # @crustjs/plugins
2
+
3
+ Official plugins for the [Crust](https://crust.cyanlabs.co) CLI framework.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add @crustjs/plugins
9
+ ```
10
+
11
+ ## Plugins
12
+
13
+ | Plugin | Description |
14
+ | --- | --- |
15
+ | `helpPlugin()` | Adds `--help` / `-h` flag and auto-generates help text |
16
+ | `versionPlugin(version)` | Adds `--version` / `-v` flag |
17
+ | `autoCompletePlugin(options?)` | Shell autocompletion support |
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import { defineCommand, runMain } from "@crustjs/core";
23
+ import { helpPlugin, versionPlugin, autoCompletePlugin } from "@crustjs/plugins";
24
+
25
+ const main = defineCommand({
26
+ meta: { name: "my-cli", description: "My CLI tool" },
27
+ run() {
28
+ console.log("Hello!");
29
+ },
30
+ });
31
+
32
+ runMain(main, {
33
+ plugins: [versionPlugin("1.0.0"), autoCompletePlugin(), helpPlugin()],
34
+ });
35
+ ```
36
+
37
+ ## Documentation
38
+
39
+ See the full docs at [crust.cyanlabs.co](https://crust.cyanlabs.co).
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,12 @@
1
+ import { CrustPlugin } from "@crustjs/core";
2
+ interface AutoCompletePluginOptions {
3
+ mode?: "error" | "help";
4
+ }
5
+ declare function autoCompletePlugin(options?: AutoCompletePluginOptions): CrustPlugin;
6
+ import { AnyCommand, CrustPlugin as CrustPlugin2 } from "@crustjs/core";
7
+ declare function renderHelp(command: AnyCommand, path: string[]): string;
8
+ declare function helpPlugin(): CrustPlugin2;
9
+ import { CrustPlugin as CrustPlugin3 } from "@crustjs/core";
10
+ type VersionValue = string | (() => string);
11
+ declare function versionPlugin(versionValue?: VersionValue): CrustPlugin3;
12
+ export { versionPlugin, renderHelp, helpPlugin, autoCompletePlugin, VersionValue, AutoCompletePluginOptions };
package/dist/index.js ADDED
@@ -0,0 +1,233 @@
1
+ // @bun
2
+ // src/autocomplete.ts
3
+ import { CrustError } from "@crustjs/core";
4
+
5
+ // src/help.ts
6
+ function formatArgToken(arg) {
7
+ const base = arg.variadic ? `${arg.name}...` : arg.name;
8
+ return arg.required ? `<${base}>` : `[${base}]`;
9
+ }
10
+ function formatUsage(meta, command, path) {
11
+ if (meta.usage)
12
+ return meta.usage;
13
+ const usageParts = [path.join(" ")];
14
+ if (command.subCommands && Object.keys(command.subCommands).length > 0 && !command.run) {
15
+ usageParts.push("<command>");
16
+ }
17
+ if (command.args) {
18
+ for (const arg of command.args) {
19
+ usageParts.push(formatArgToken(arg));
20
+ }
21
+ }
22
+ if (command.flags && Object.keys(command.flags).length > 0) {
23
+ usageParts.push("[options]");
24
+ }
25
+ return usageParts.join(" ");
26
+ }
27
+ function formatFlagName(name, def) {
28
+ if (!def.alias)
29
+ return `--${name}`;
30
+ const aliases = Array.isArray(def.alias) ? def.alias : [def.alias];
31
+ const shortAlias = aliases.find((alias) => alias.length === 1);
32
+ if (shortAlias) {
33
+ return `-${shortAlias}, --${name}`;
34
+ }
35
+ return `--${name}`;
36
+ }
37
+ function formatFlagsSection(flagsDef) {
38
+ if (!flagsDef || Object.keys(flagsDef).length === 0)
39
+ return [];
40
+ const lines = ["OPTIONS:"];
41
+ for (const [name, def] of Object.entries(flagsDef)) {
42
+ const rendered = formatFlagName(name, def).padEnd(18, " ");
43
+ lines.push(` ${rendered}${def.description ?? ""}`.trimEnd());
44
+ }
45
+ return lines;
46
+ }
47
+ function formatArgsSection(command) {
48
+ if (!command.args || command.args.length === 0)
49
+ return [];
50
+ const lines = ["ARGS:"];
51
+ for (const arg of command.args) {
52
+ const rendered = formatArgToken(arg).padEnd(18, " ");
53
+ lines.push(` ${rendered}${arg.description ?? ""}`.trimEnd());
54
+ }
55
+ return lines;
56
+ }
57
+ function formatCommandsSection(command) {
58
+ if (!command.subCommands || Object.keys(command.subCommands).length === 0) {
59
+ return [];
60
+ }
61
+ const lines = ["COMMANDS:"];
62
+ for (const [name, subCommand] of Object.entries(command.subCommands)) {
63
+ const rendered = name.padEnd(10, " ");
64
+ lines.push(` ${rendered}${subCommand.meta.description ?? ""}`.trimEnd());
65
+ }
66
+ return lines;
67
+ }
68
+ function renderHelp(command, path) {
69
+ const lines = [];
70
+ lines.push(command.meta.description ? `${path.join(" ")} - ${command.meta.description}` : path.join(" "));
71
+ lines.push("");
72
+ lines.push("USAGE:");
73
+ lines.push(` ${formatUsage(command.meta, command, path)}`);
74
+ const commandsSection = formatCommandsSection(command);
75
+ if (commandsSection.length > 0) {
76
+ lines.push("");
77
+ lines.push(...commandsSection);
78
+ }
79
+ const argsSection = formatArgsSection(command);
80
+ if (argsSection.length > 0) {
81
+ lines.push("");
82
+ lines.push(...argsSection);
83
+ }
84
+ const optionsSection = formatFlagsSection(command.flags);
85
+ if (optionsSection.length > 0) {
86
+ lines.push("");
87
+ lines.push(...optionsSection);
88
+ }
89
+ return lines.join(`
90
+ `);
91
+ }
92
+ var helpFlagDef = {
93
+ type: Boolean,
94
+ alias: "h",
95
+ description: "Show help"
96
+ };
97
+ function injectHelpFlags(command, addFlag) {
98
+ addFlag(command, "help", helpFlagDef);
99
+ if (command.subCommands) {
100
+ for (const sub of Object.values(command.subCommands)) {
101
+ injectHelpFlags(sub, addFlag);
102
+ }
103
+ }
104
+ }
105
+ function helpPlugin() {
106
+ return {
107
+ name: "help",
108
+ setup(context, actions) {
109
+ injectHelpFlags(context.rootCommand, actions.addFlag);
110
+ },
111
+ async middleware(context, next) {
112
+ if (!context.route) {
113
+ await next();
114
+ return;
115
+ }
116
+ const routedCommand = context.route.command;
117
+ const shouldShowHelp = context.input?.flags.help === true;
118
+ if (!shouldShowHelp && routedCommand.run) {
119
+ await next();
120
+ return;
121
+ }
122
+ console.log(renderHelp(routedCommand, [...context.route.commandPath]));
123
+ }
124
+ };
125
+ }
126
+
127
+ // src/autocomplete.ts
128
+ function levenshtein(a, b) {
129
+ const aLen = a.length;
130
+ const bLen = b.length;
131
+ if (aLen === 0)
132
+ return bLen;
133
+ if (bLen === 0)
134
+ return aLen;
135
+ const row = Array.from({ length: bLen + 1 }, (_, i) => i);
136
+ for (let i = 1;i <= aLen; i++) {
137
+ let prev = i;
138
+ for (let j = 1;j <= bLen; j++) {
139
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
140
+ const val = Math.min(row[j] + 1, prev + 1, row[j - 1] + cost);
141
+ row[j - 1] = prev;
142
+ prev = val;
143
+ }
144
+ row[bLen] = prev;
145
+ }
146
+ return row[bLen];
147
+ }
148
+ function findSuggestions(input, candidates) {
149
+ const suggestions = [];
150
+ for (const candidate of candidates) {
151
+ if (candidate.startsWith(input) || input.startsWith(candidate)) {
152
+ suggestions.push({ name: candidate, distance: 0 });
153
+ continue;
154
+ }
155
+ const distance = levenshtein(input, candidate);
156
+ if (distance <= 3) {
157
+ suggestions.push({ name: candidate, distance });
158
+ }
159
+ }
160
+ suggestions.sort((a, b) => {
161
+ if (a.distance !== b.distance)
162
+ return a.distance - b.distance;
163
+ return a.name.localeCompare(b.name);
164
+ });
165
+ return suggestions.map((suggestion) => suggestion.name);
166
+ }
167
+ function autoCompletePlugin(options = {}) {
168
+ const mode = options.mode ?? "error";
169
+ return {
170
+ name: "autocomplete",
171
+ async middleware(_context, next) {
172
+ try {
173
+ await next();
174
+ return;
175
+ } catch (error) {
176
+ if (!(error instanceof CrustError))
177
+ throw error;
178
+ if (!error.is("COMMAND_NOT_FOUND"))
179
+ throw error;
180
+ const details = error.details;
181
+ const suggestions = findSuggestions(details.input, details.available);
182
+ let message = `Unknown command "${details.input}".`;
183
+ if (suggestions.length > 0) {
184
+ message += ` Did you mean "${suggestions[0]}"?`;
185
+ }
186
+ if (mode === "help") {
187
+ console.log(message);
188
+ console.log("");
189
+ console.log(renderHelp(details.parentCommand, details.commandPath));
190
+ return;
191
+ }
192
+ if (details.available.length > 0) {
193
+ message += `
194
+
195
+ Available commands: ${details.available.join(", ")}`;
196
+ }
197
+ console.error(message);
198
+ process.exitCode = 1;
199
+ }
200
+ }
201
+ };
202
+ }
203
+ // src/version.ts
204
+ function versionPlugin(versionValue = "0.0.0") {
205
+ return {
206
+ name: "version",
207
+ setup(context, actions) {
208
+ actions.addFlag(context.rootCommand, "version", {
209
+ type: Boolean,
210
+ alias: "v",
211
+ description: "Show version number"
212
+ });
213
+ },
214
+ async middleware(context, next) {
215
+ if (!context.route || context.route.command !== context.rootCommand) {
216
+ await next();
217
+ return;
218
+ }
219
+ if (!context.input?.flags.version) {
220
+ await next();
221
+ return;
222
+ }
223
+ const version = typeof versionValue === "function" ? versionValue() : versionValue;
224
+ console.log(`${context.rootCommand.meta.name} v${version}`);
225
+ }
226
+ };
227
+ }
228
+ export {
229
+ versionPlugin,
230
+ renderHelp,
231
+ helpPlugin,
232
+ autoCompletePlugin
233
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@crustjs/plugins",
3
+ "version": "0.0.2",
4
+ "description": "Official plugins for the Crust CLI framework",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "chenxin-yan",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/chenxin-yan/crust.git",
11
+ "directory": "packages/plugins"
12
+ },
13
+ "homepage": "https://crust.cyanlabs.co",
14
+ "bugs": {
15
+ "url": "https://github.com/chenxin-yan/crust/issues"
16
+ },
17
+ "keywords": [
18
+ "cli",
19
+ "plugin",
20
+ "help",
21
+ "version",
22
+ "autocomplete",
23
+ "bun",
24
+ "typescript"
25
+ ],
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "import": "./dist/index.js",
32
+ "types": "./dist/index.d.ts"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "bunup",
40
+ "dev": "bunup --watch",
41
+ "check:types": "tsc --noEmit",
42
+ "test": "bun test"
43
+ },
44
+ "devDependencies": {
45
+ "@crustjs/config": "workspace:*",
46
+ "@crustjs/core": "workspace:*",
47
+ "bunup": "catalog:"
48
+ },
49
+ "peerDependencies": {
50
+ "@crustjs/core": "workspace:*",
51
+ "typescript": "catalog:"
52
+ }
53
+ }