@oclif/plugin-search 1.2.11 → 1.2.12-dev.0

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 CHANGED
@@ -22,7 +22,7 @@ $ npm install -g @oclif/plugin-search
22
22
  $ @oclif/plugin-search COMMAND
23
23
  running command...
24
24
  $ @oclif/plugin-search (--version)
25
- @oclif/plugin-search/1.2.11 linux-x64 node-v20.17.0
25
+ @oclif/plugin-search/1.2.12-dev.0 linux-x64 node-v20.17.0
26
26
  $ @oclif/plugin-search --help [COMMAND]
27
27
  USAGE
28
28
  $ @oclif/plugin-search COMMAND
@@ -41,7 +41,11 @@ Search for a command.
41
41
 
42
42
  ```
43
43
  USAGE
44
- $ @oclif/plugin-search search
44
+ $ @oclif/plugin-search search [-a help|copy|doctor|source|npm]
45
+
46
+ FLAGS
47
+ -a, --action=<option> Action to take on the selected command
48
+ <options: help|copy|doctor|source|npm>
45
49
 
46
50
  DESCRIPTION
47
51
  Search for a command.
@@ -49,7 +53,7 @@ DESCRIPTION
49
53
  Once you select a command, hit enter and it will show the help for that command.
50
54
  ```
51
55
 
52
- _See code: [src/commands/search.ts](https://github.com/oclif/plugin-search/blob/v1.2.11/src/commands/search.ts)_
56
+ _See code: [src/commands/search.ts](https://github.com/oclif/plugin-search/blob/v1.2.12-dev.0/src/commands/search.ts)_
53
57
  <!-- commandsstop -->
54
58
 
55
59
  # Contributing
@@ -1,6 +1,10 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export default class Search extends Command {
3
3
  static description: string;
4
+ static readonly flags: {
5
+ action: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ };
7
+ static readonly strict = false;
4
8
  static summary: string;
5
9
  run(): Promise<unknown>;
6
10
  }
@@ -4,15 +4,36 @@
4
4
  * Licensed under the BSD 3-Clause license.
5
5
  * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
- import { Command, loadHelpClass, toConfiguredId, toStandardizedId, ux } from '@oclif/core';
8
- import ansiEscapes from 'ansi-escapes';
9
- import autocomplete from 'inquirer-autocomplete-standalone';
7
+ import search from '@inquirer/search';
8
+ import { Command, Flags, loadHelpClass, toConfiguredId, toStandardizedId, ux } from '@oclif/core';
9
+ import { bold } from 'ansis';
10
+ import clipboard from 'clipboardy';
11
+ import fuzzysort from 'fuzzysort';
12
+ import { got } from 'got';
10
13
  import readline from 'node:readline';
14
+ import open from 'open';
11
15
  export default class Search extends Command {
12
16
  static description = 'Once you select a command, hit enter and it will show the help for that command.';
17
+ static flags = {
18
+ action: Flags.string({
19
+ char: 'a',
20
+ options: ['help', 'copy', 'doctor', 'source', 'npm'],
21
+ summary: 'Action to take on the selected command',
22
+ }),
23
+ };
24
+ static strict = false;
13
25
  static summary = 'Search for a command.';
14
26
  async run() {
15
- this.log(ux.colorize('cyan', `Use ${ux.colorize('bold', '↑')} and ${ux.colorize('bold', '↓')} keys or type to search for a command.\nPress ${ux.colorize('bold', 'ENTER')} to view help. Press ${ux.colorize('bold', 'ESC')} to exit.\n`));
27
+ const { argv, flags } = await this.parse(Search);
28
+ // Join the args together into a string so you can pass something like 'dep sta' to fuzzy search
29
+ const args = argv.join(' ');
30
+ const theme = {
31
+ helpMode: 'never',
32
+ prefix: ux.colorize('cyan', `Use ${ux.colorize('bold', '↑')} and ${ux.colorize('bold', '↓')} keys or type to search for a command.\nPress ${ux.colorize('bold', 'ENTER')} to view help. Press ${ux.colorize('bold', 'ESC')} to exit.\n\n`),
33
+ style: {
34
+ description: (desc) => `\n${ux.colorize('cyan', desc)}`, // Give the description a little extra padding
35
+ },
36
+ };
16
37
  const commandChoices = this.config.commands
17
38
  .filter((c) => !c.hidden && !c.aliases.includes(c.id))
18
39
  .sort((a, b) => a.id.localeCompare(b.id))
@@ -25,26 +46,37 @@ export default class Search extends Command {
25
46
  };
26
47
  });
27
48
  const pageSize = Math.floor(process.stdout.rows < 20 ? process.stdout.rows / 2 : 10);
28
- const commandPromise = autocomplete({
29
- emptyText: 'Nothing found!',
49
+ const commandPromise = search({
30
50
  message: 'Search for a command',
31
51
  pageSize,
32
52
  async source(input) {
33
- return input ? commandChoices.filter((c) => c.name.includes(input)) : commandChoices;
53
+ // TODO: There is a bug here somewhere:
54
+ // - pass an arg
55
+ // - hit down arrow
56
+ // - hit any other character with clear the input
57
+ // - hitting delete will clear input, but keep the fuzzy results
58
+ if (input === undefined && args)
59
+ input = args;
60
+ const results = fuzzysort.go(input, commandChoices, { all: true, key: 'name' });
61
+ return results.map((r) => ({
62
+ description: r.obj.description,
63
+ name: r.highlight(bold.open, bold.close),
64
+ value: r.obj.value,
65
+ }));
34
66
  },
35
- });
36
- function cancel() {
37
- commandPromise.cancel();
38
- // erase the list of commands
39
- process.stdout.write(ansiEscapes.eraseLines(pageSize + 3));
40
- }
67
+ // @ts-expect-error Not sure why this is complaining about the helpMode type
68
+ theme,
69
+ }, { clearPromptOnDone: true });
41
70
  readline.emitKeypressEvents(process.stdin);
42
71
  process.stdin.setRawMode(true);
72
+ // If args were passed in, we "replay" the key presses to populate the search
73
+ if (args)
74
+ process.stdin.emit('data', args);
75
+ // Allow the user to exit the search with the escape key or with ctrl+c
43
76
  process.stdin.on('keypress', (_, key) => {
44
- if (key.name === 'escape')
45
- cancel();
46
- if (key.name === 'c' && key.ctrl)
47
- cancel();
77
+ if ((key.name === 'escape' || (key.name === 'c' && key.ctrl)) && commandPromise) {
78
+ commandPromise.cancel();
79
+ }
48
80
  });
49
81
  const command = await commandPromise
50
82
  .catch((error) => {
@@ -55,8 +87,120 @@ export default class Search extends Command {
55
87
  .then((result) => result);
56
88
  if (!command)
57
89
  return;
58
- const Help = await loadHelpClass(this.config);
59
- const help = new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions);
60
- return help.showHelp([toStandardizedId(command, this.config)]);
90
+ // eslint-disable-next-line unicorn/consistent-function-scoping
91
+ const getPluginDetails = (command) => {
92
+ const commandId = toStandardizedId(command, this.config);
93
+ const commandConfig = this.config.findCommand(commandId);
94
+ const pluginName = commandConfig?.pluginName;
95
+ if (!pluginName)
96
+ this.error('Key `pluginName` not found in the config for this command.');
97
+ const commandPjson = this.config.plugins.get(pluginName)?.pjson;
98
+ const homepage = commandPjson?.homepage;
99
+ // TODO: add a check for homepage
100
+ const commandVersion = commandPjson?.version;
101
+ return { commandConfig, commandId, commandPjson, commandVersion, homepage, pluginName };
102
+ };
103
+ const getSourceUrl = async (homepage, commandId, commandVersion) => {
104
+ if (!homepage)
105
+ return;
106
+ const commandToPath = `${commandId.replaceAll(':', '/')}.ts`;
107
+ // TODO: do we need to take into account directory level index.ts command files?
108
+ // TODO: talk to Mike about dynamically built command paths
109
+ const urls = [
110
+ `/blob/${commandVersion}/src/commands/${commandToPath}`,
111
+ `/blob/v${commandVersion}/src/commands/${commandToPath}`,
112
+ `/blob/-/src/commands/${commandToPath}`,
113
+ `/blob/main/src/commands/${commandToPath}`,
114
+ `/blob/master/src/commands/${commandToPath}`,
115
+ ];
116
+ const responses = await Promise.all(urls.map((url) => got(`${homepage}${url}`, { throwHttpErrors: false })));
117
+ return responses.find((r) => r.statusCode === 200)?.url ?? undefined;
118
+ };
119
+ const { commandId, commandVersion, homepage, pluginName } = getPluginDetails(command);
120
+ const sourceUrl = await getSourceUrl(homepage, commandId, commandVersion);
121
+ let actionPrompt;
122
+ if (!flags.action) {
123
+ const actions = [
124
+ {
125
+ description: 'Show the help text for this command',
126
+ name: 'Show help',
127
+ value: 'help',
128
+ },
129
+ {
130
+ description: 'Copy the command to your clipboard',
131
+ name: 'Copy command',
132
+ value: 'copy',
133
+ },
134
+ {
135
+ description: 'Copy the command to your clipboard for use with the doctor command',
136
+ name: 'Copy command for doctor',
137
+ value: 'doctor',
138
+ },
139
+ {
140
+ description: 'Open the source code for this command on GitHub',
141
+ disabled: sourceUrl ? false : '(Unable to resolve source code URL)',
142
+ name: 'Go to source code',
143
+ value: 'source',
144
+ },
145
+ {
146
+ description: 'View the npm details for this package',
147
+ name: 'View package on npm',
148
+ value: 'npm',
149
+ },
150
+ ];
151
+ const actionPromise = search({
152
+ message: `Select an action for: ${ux.colorize('dim', '$ sf ' + command)}`,
153
+ pageSize,
154
+ async source(input) {
155
+ const results = fuzzysort.go(input, actions, { all: true, key: 'name' });
156
+ return results.map((r) => ({
157
+ description: r.obj.description,
158
+ disabled: r.obj.disabled,
159
+ name: r.highlight(bold.open, bold.close),
160
+ value: r.obj.value,
161
+ }));
162
+ },
163
+ // @ts-expect-error Not sure why this is complaining about the helpMode type
164
+ theme,
165
+ }, { clearPromptOnDone: true });
166
+ // Allow the user to exit the search with the escape key or with ctrl+c
167
+ process.stdin.on('keypress', (_, key) => {
168
+ if ((key.name === 'escape' || (key.name === 'c' && key.ctrl)) && actionPromise) {
169
+ actionPromise.cancel();
170
+ }
171
+ });
172
+ actionPrompt = await actionPromise
173
+ .catch((error) => {
174
+ if (error.message === 'Prompt was canceled')
175
+ return;
176
+ throw error;
177
+ })
178
+ .then((result) => result);
179
+ }
180
+ switch (flags.action ?? actionPrompt) {
181
+ case 'help': {
182
+ const Help = await loadHelpClass(this.config);
183
+ const help = new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions);
184
+ return help.showHelp([toStandardizedId(command, this.config)]);
185
+ }
186
+ case 'copy': {
187
+ clipboard.writeSync(`sf ${command} `);
188
+ this.log(ux.colorize('green', 'Command copied to clipboard!'));
189
+ break;
190
+ }
191
+ case 'doctor': {
192
+ clipboard.writeSync(`sf doctor --command "${command}"`);
193
+ this.log(ux.colorize('green', 'Command copied to clipboard!'));
194
+ break;
195
+ }
196
+ case 'npm': {
197
+ open(`https://www.npmjs.com/package/${pluginName}/v/${commandVersion}`);
198
+ break;
199
+ }
200
+ case 'source': {
201
+ open(sourceUrl);
202
+ break;
203
+ }
204
+ }
61
205
  }
62
206
  }
@@ -4,14 +4,30 @@
4
4
  "aliases": [],
5
5
  "args": {},
6
6
  "description": "Once you select a command, hit enter and it will show the help for that command.",
7
- "flags": {},
7
+ "flags": {
8
+ "action": {
9
+ "char": "a",
10
+ "name": "action",
11
+ "summary": "Action to take on the selected command",
12
+ "hasDynamicHelp": false,
13
+ "multiple": false,
14
+ "options": [
15
+ "help",
16
+ "copy",
17
+ "doctor",
18
+ "source",
19
+ "npm"
20
+ ],
21
+ "type": "option"
22
+ }
23
+ },
8
24
  "hasDynamicHelp": false,
9
25
  "hiddenAliases": [],
10
26
  "id": "search",
11
27
  "pluginAlias": "@oclif/plugin-search",
12
28
  "pluginName": "@oclif/plugin-search",
13
29
  "pluginType": "core",
14
- "strict": true,
30
+ "strict": false,
15
31
  "summary": "Search for a command.",
16
32
  "enableJsonFlag": false,
17
33
  "isESM": true,
@@ -26,5 +42,5 @@
26
42
  ]
27
43
  }
28
44
  },
29
- "version": "1.2.11"
45
+ "version": "1.2.12-dev.0"
30
46
  }
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@oclif/plugin-search",
3
- "version": "1.2.11",
3
+ "version": "1.2.12-dev.0",
4
4
  "description": "A command for searching commands",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/plugin-search/issues",
7
7
  "dependencies": {
8
+ "@inquirer/search": "^2.0.1",
8
9
  "@oclif/core": "^4",
9
- "ansi-escapes": "^7.0.0",
10
- "inquirer-autocomplete-standalone": "^0.8.1"
10
+ "ansis": "^3.3.2",
11
+ "clipboardy": "^4.0.0",
12
+ "fuzzysort": "^3.0.2",
13
+ "got": "^13.0.0",
14
+ "open": "^10.1.0"
11
15
  },
12
16
  "devDependencies": {
13
17
  "@commitlint/config-conventional": "^19",
@@ -27,7 +31,7 @@
27
31
  "husky": "^9.1.6",
28
32
  "lint-staged": "^15",
29
33
  "mocha": "^10.7.3",
30
- "oclif": "^4.14.35",
34
+ "oclif": "^4.15.3",
31
35
  "prettier": "^3.3.3",
32
36
  "shx": "^0.3.3",
33
37
  "ts-node": "^10.9.2",