@evantahler/mcpx 0.18.2 → 0.18.5
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/package.json +63 -62
- package/src/cli.ts +46 -54
- package/src/client/browser.ts +15 -15
- package/src/client/debug-fetch.ts +53 -56
- package/src/client/elicitation.ts +279 -291
- package/src/client/http.ts +1 -1
- package/src/client/manager.ts +481 -514
- package/src/client/oauth.ts +272 -282
- package/src/client/sse.ts +1 -1
- package/src/client/stdio.ts +7 -7
- package/src/client/trace.ts +146 -152
- package/src/client/transport-options.ts +20 -20
- package/src/commands/add.ts +160 -165
- package/src/commands/allow.ts +141 -142
- package/src/commands/auth.ts +86 -90
- package/src/commands/check-update.ts +49 -53
- package/src/commands/deny.ts +114 -117
- package/src/commands/exec.ts +218 -222
- package/src/commands/index.ts +41 -41
- package/src/commands/info.ts +48 -50
- package/src/commands/list.ts +49 -49
- package/src/commands/ping.ts +47 -50
- package/src/commands/prompt.ts +40 -50
- package/src/commands/remove.ts +54 -56
- package/src/commands/resource.ts +31 -36
- package/src/commands/search.ts +35 -39
- package/src/commands/servers.ts +44 -48
- package/src/commands/skill.ts +89 -95
- package/src/commands/task.ts +50 -60
- package/src/commands/upgrade.ts +191 -208
- package/src/commands/with-command.ts +27 -29
- package/src/config/env.ts +26 -28
- package/src/config/loader.ts +99 -102
- package/src/config/schemas.ts +78 -87
- package/src/constants.ts +17 -17
- package/src/context.ts +51 -51
- package/src/lib/client-settings.ts +127 -140
- package/src/lib/input.ts +23 -26
- package/src/output/format-output.ts +12 -16
- package/src/output/format-table.ts +39 -42
- package/src/output/formatter.ts +790 -814
- package/src/output/logger.ts +140 -152
- package/src/sdk.ts +283 -291
- package/src/search/index.ts +50 -54
- package/src/search/indexer.ts +65 -65
- package/src/search/keyword.ts +54 -54
- package/src/search/semantic.ts +39 -39
- package/src/search/staleness.ts +3 -3
- package/src/search/types.ts +4 -4
- package/src/update/background.ts +51 -51
- package/src/update/cache.ts +21 -21
- package/src/update/checker.ts +81 -86
- package/src/validation/schema.ts +52 -58
package/src/commands/auth.ts
CHANGED
|
@@ -1,114 +1,110 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { getContext } from "../context.ts";
|
|
3
|
-
import { isHttpServer } from "../config/schemas.ts";
|
|
4
|
-
import { saveAuth } from "../config/loader.ts";
|
|
5
2
|
import { McpOAuthProvider, runOAuthFlow } from "../client/oauth.ts";
|
|
3
|
+
import { saveAuth } from "../config/loader.ts";
|
|
4
|
+
import { isHttpServer } from "../config/schemas.ts";
|
|
5
|
+
import { getContext } from "../context.ts";
|
|
6
6
|
import { logger } from "../output/logger.ts";
|
|
7
7
|
import { runIndex } from "./index.ts";
|
|
8
8
|
|
|
9
9
|
export function registerAuthCommand(program: Command) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const { config, formatOptions } = await getContext(program);
|
|
10
|
+
program
|
|
11
|
+
.command("auth <server>")
|
|
12
|
+
.description("authenticate with an HTTP MCP server")
|
|
13
|
+
.option("-s, --status", "check auth status and token TTL")
|
|
14
|
+
.option("-r, --refresh", "force token refresh")
|
|
15
|
+
.option("--no-index", "skip rebuilding the search index after auth")
|
|
16
|
+
.action(async (server: string, options: { status?: boolean; refresh?: boolean; index?: boolean }) => {
|
|
17
|
+
const { config, formatOptions } = await getContext(program);
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
19
|
+
const serverConfig = config.servers.mcpServers[server];
|
|
20
|
+
if (!serverConfig) {
|
|
21
|
+
console.error(`Unknown server: "${server}"`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (!isHttpServer(serverConfig)) {
|
|
25
|
+
console.error(`Server "${server}" is not an HTTP server — OAuth only applies to HTTP servers`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const provider = new McpOAuthProvider({
|
|
30
|
+
serverName: server,
|
|
31
|
+
configDir: config.configDir,
|
|
32
|
+
auth: config.auth,
|
|
33
|
+
});
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
if (options.status) {
|
|
36
|
+
showStatus(server, provider);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
if (options.refresh) {
|
|
41
|
+
const spinner = logger.startSpinner(`Refreshing token for "${server}"…`, formatOptions);
|
|
42
|
+
try {
|
|
43
|
+
await provider.refreshIfNeeded(serverConfig.url);
|
|
44
|
+
spinner.success(`Token refreshed for "${server}"`);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
spinner.error(`Refresh failed: ${err instanceof Error ? err.message : err}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
// Default: full OAuth flow
|
|
53
|
+
const spinner = logger.startSpinner(`Authenticating with "${server}"…`, formatOptions);
|
|
54
|
+
try {
|
|
55
|
+
await runOAuthFlow(serverConfig.url, provider);
|
|
56
|
+
spinner.success(`Authenticated with "${server}"`);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
spinner.error(`Authentication failed: ${err instanceof Error ? err.message : err}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
62
|
+
if (options.index !== false) {
|
|
63
|
+
await runIndex(program);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
export function registerDeauthCommand(program: Command) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
program
|
|
70
|
+
.command("deauth <server>")
|
|
71
|
+
.description("remove stored authentication for a server")
|
|
72
|
+
.action(async (server: string) => {
|
|
73
|
+
const { config } = await getContext(program);
|
|
78
74
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
if (!config.auth[server]) {
|
|
76
|
+
console.log(`No auth stored for "${server}"`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
delete config.auth[server];
|
|
81
|
+
await saveAuth(config.configDir, config.auth);
|
|
82
|
+
console.log(`Deauthenticated "${server}"`);
|
|
83
|
+
});
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
function showStatus(server: string, provider: McpOAuthProvider) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
if (!provider.isComplete()) {
|
|
88
|
+
console.log(`${server}: not authenticated`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
const expired = provider.isExpired();
|
|
93
|
+
const hasRefresh = provider.hasRefreshToken();
|
|
94
|
+
const status = expired ? "expired" : "authenticated";
|
|
99
95
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
console.log(`${server}: ${status}`);
|
|
97
|
+
if (hasRefresh) {
|
|
98
|
+
console.log(" refresh token: present");
|
|
99
|
+
}
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
if (!expired) {
|
|
102
|
+
// Show TTL if we have expires_at from the auth entry
|
|
103
|
+
const entry = provider.auth[server];
|
|
104
|
+
if (entry?.expires_at) {
|
|
105
|
+
const remaining = new Date(entry.expires_at).getTime() - Date.now();
|
|
106
|
+
const minutes = Math.round(remaining / 60000);
|
|
107
|
+
console.log(` expires in: ${minutes} minutes`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
114
110
|
}
|
|
@@ -1,70 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cyan, dim, green, yellow } from "ansis";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
import { createSpinner } from "nanospinner";
|
|
4
4
|
import pkg from "../../package.json";
|
|
5
|
-
import { checkForUpdate } from "../update/checker.ts";
|
|
6
5
|
import { saveUpdateCache } from "../update/cache.ts";
|
|
7
6
|
import type { UpdateCache } from "../update/checker.ts";
|
|
7
|
+
import { checkForUpdate } from "../update/checker.ts";
|
|
8
8
|
|
|
9
9
|
export function registerCheckUpdateCommand(program: Command) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
program
|
|
11
|
+
.command("check-update")
|
|
12
|
+
.description("Check for a newer version of mcpx")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
const opts = program.opts();
|
|
15
|
+
const json = !!(opts.json as boolean | undefined);
|
|
16
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
? createSpinner("Checking for updates...", { stream: process.stderr }).start()
|
|
21
|
-
: null;
|
|
18
|
+
const spinner =
|
|
19
|
+
!json && isTTY ? createSpinner("Checking for updates...", { stream: process.stderr }).start() : null;
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
try {
|
|
22
|
+
const info = await checkForUpdate(pkg.version);
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
// Save to cache
|
|
25
|
+
const cache: UpdateCache = {
|
|
26
|
+
lastCheckAt: new Date().toISOString(),
|
|
27
|
+
latestVersion: info.latestVersion,
|
|
28
|
+
hasUpdate: info.hasUpdate,
|
|
29
|
+
changelog: info.changelog,
|
|
30
|
+
};
|
|
31
|
+
await saveUpdateCache(cache);
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
spinner?.stop();
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
if (json) {
|
|
36
|
+
console.log(JSON.stringify(info, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
40
|
+
if (!info.hasUpdate) {
|
|
41
|
+
if (info.aheadOfLatest) {
|
|
42
|
+
console.log(
|
|
43
|
+
yellow(`mcpx v${info.currentVersion} is ahead of latest published release (v${info.latestVersion})`),
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(green(`mcpx is up to date (v${info.currentVersion})`));
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
console.log(yellow(`Update available: ${info.currentVersion} → ${info.latestVersion}`));
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
if (info.changelog) {
|
|
54
|
+
console.log("");
|
|
55
|
+
console.log(dim(info.changelog));
|
|
56
|
+
}
|
|
61
57
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(cyan(`Run \`mcpx upgrade\` to update`));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
spinner?.error({ text: "Failed to check for updates" });
|
|
62
|
+
console.error(String(err));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
70
66
|
}
|
package/src/commands/deny.ts
CHANGED
|
@@ -1,134 +1,131 @@
|
|
|
1
|
+
import { bold, dim, red, yellow } from "ansis";
|
|
1
2
|
import type { Command } from "commander";
|
|
2
|
-
import { bold, dim, green, red, yellow } from "ansis";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
getServerPatterns,
|
|
4
|
+
type Client,
|
|
5
|
+
execPattern,
|
|
6
|
+
getServerPatterns,
|
|
7
|
+
readClientSettings,
|
|
8
|
+
readOnlyPatterns,
|
|
9
|
+
removeAllMcpxPatterns,
|
|
10
|
+
removePatterns,
|
|
11
|
+
resolveSettingsPath,
|
|
12
|
+
type Scope,
|
|
13
|
+
writeClientSettings,
|
|
15
14
|
} from "../lib/client-settings.ts";
|
|
16
15
|
import { formatOutput } from "../output/format-output.ts";
|
|
17
16
|
import type { FormatOptions } from "../output/formatter.ts";
|
|
18
17
|
|
|
19
18
|
export function registerDenyCommand(program: Command) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
19
|
+
program
|
|
20
|
+
.command("deny")
|
|
21
|
+
.description("remove permission rules for mcpx commands (Claude Code or Cursor)")
|
|
22
|
+
.argument("[server]", "server name to deny")
|
|
23
|
+
.argument("[tools...]", "specific tool names to deny")
|
|
24
|
+
.option("--all", "remove all mcpx-related permissions")
|
|
25
|
+
.option("--all-read", "remove read-only command permissions")
|
|
26
|
+
.option("--cursor", "target Cursor settings instead of Claude Code")
|
|
27
|
+
.option("--local", "write to local settings (default)")
|
|
28
|
+
.option("--project", "write to project settings (shared)")
|
|
29
|
+
.option("--global", "write to global settings")
|
|
30
|
+
.option("--dry-run", "show what would be removed")
|
|
31
|
+
.action(
|
|
32
|
+
async (
|
|
33
|
+
server: string | undefined,
|
|
34
|
+
tools: string[],
|
|
35
|
+
options: {
|
|
36
|
+
all?: boolean;
|
|
37
|
+
allRead?: boolean;
|
|
38
|
+
cursor?: boolean;
|
|
39
|
+
local?: boolean;
|
|
40
|
+
project?: boolean;
|
|
41
|
+
global?: boolean;
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
},
|
|
44
|
+
) => {
|
|
45
|
+
const formatOptions: FormatOptions = { json: program.opts().json };
|
|
46
|
+
const client: Client = options.cursor ? "cursor" : "claude";
|
|
47
|
+
const scope: Scope = options.global ? "global" : options.project ? "project" : "local";
|
|
48
|
+
const path = resolveSettingsPath(scope, client);
|
|
49
|
+
const settings = await readClientSettings(path);
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
let result: { settings: typeof settings; removed: string[] };
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
if (options.all) {
|
|
54
|
+
// Remove all mcpx-related patterns
|
|
55
|
+
result = removeAllMcpxPatterns(settings, client);
|
|
56
|
+
} else {
|
|
57
|
+
// Build the list of patterns to remove
|
|
58
|
+
const patterns: string[] = [];
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if (options.allRead) {
|
|
61
|
+
patterns.push(...readOnlyPatterns(client));
|
|
62
|
+
}
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
if (server && tools.length > 0) {
|
|
65
|
+
for (const tool of tools) {
|
|
66
|
+
patterns.push(execPattern(server, tool, client));
|
|
67
|
+
}
|
|
68
|
+
} else if (server) {
|
|
69
|
+
// Remove the server-level pattern AND all tool-specific patterns for this server
|
|
70
|
+
patterns.push(execPattern(server, undefined, client));
|
|
71
|
+
patterns.push(...getServerPatterns(settings, server, client));
|
|
72
|
+
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
if (patterns.length === 0) {
|
|
75
|
+
console.error("error: specify a server, --all, or --all-read. See 'mcpx deny --help'.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
result = removePatterns(settings, patterns);
|
|
80
|
+
}
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
82
|
+
if (options.dryRun) {
|
|
83
|
+
console.log(
|
|
84
|
+
formatOutput(
|
|
85
|
+
{ scope, path, wouldRemove: result.removed },
|
|
86
|
+
() => {
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
lines.push(bold("Dry run") + dim(` — would remove from ${path}:`));
|
|
89
|
+
if (result.removed.length === 0) {
|
|
90
|
+
lines.push(` ${dim("(no matching patterns found)")}`);
|
|
91
|
+
} else {
|
|
92
|
+
for (const p of result.removed) {
|
|
93
|
+
lines.push(` ${yellow("-")} ${p}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return lines.join("\n");
|
|
97
|
+
},
|
|
98
|
+
formatOptions,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
await writeClientSettings(path, result.settings);
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
);
|
|
106
|
+
console.log(
|
|
107
|
+
formatOutput(
|
|
108
|
+
{
|
|
109
|
+
scope,
|
|
110
|
+
path,
|
|
111
|
+
removed: result.removed,
|
|
112
|
+
total: (result.settings.permissions?.allow ?? []).length,
|
|
113
|
+
},
|
|
114
|
+
() => {
|
|
115
|
+
const lines: string[] = [];
|
|
116
|
+
if (result.removed.length === 0) {
|
|
117
|
+
lines.push(dim("No matching patterns found — no changes."));
|
|
118
|
+
} else {
|
|
119
|
+
lines.push(bold(`Removed ${result.removed.length} permission(s)`) + dim(` → ${path}`));
|
|
120
|
+
for (const p of result.removed) {
|
|
121
|
+
lines.push(` ${red("-")} ${p}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
},
|
|
126
|
+
formatOptions,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
134
131
|
}
|