@evantahler/mcpx 0.15.4 → 0.15.9
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/.claude/skills/mcpx.md +115 -75
- package/package.json +1 -1
- package/src/client/http.ts +3 -22
- package/src/client/manager.ts +54 -66
- package/src/client/sse.ts +3 -14
- package/src/client/transport-options.ts +31 -0
- package/src/commands/exec.ts +2 -1
- package/src/commands/index.ts +15 -20
- package/src/commands/info.ts +47 -52
- package/src/commands/list.ts +49 -54
- package/src/commands/prompt.ts +16 -17
- package/src/commands/resource.ts +28 -32
- package/src/commands/search.ts +2 -1
- package/src/commands/servers.ts +6 -12
- package/src/commands/task.ts +48 -64
- package/src/commands/with-command.ts +59 -0
- package/src/config/env.ts +4 -2
- package/src/config/loader.ts +10 -4
- package/src/constants.ts +19 -0
- package/src/context.ts +7 -6
- package/src/output/format-output.ts +18 -0
- package/src/output/format-table.ts +63 -0
- package/src/output/formatter.ts +424 -570
- package/src/search/index.ts +2 -1
- package/src/search/keyword.ts +2 -5
- package/src/search/semantic.ts +4 -7
- package/src/search/types.ts +7 -0
- package/src/validation/schema.ts +18 -30
package/src/commands/info.ts
CHANGED
|
@@ -1,63 +1,58 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { Tool, Resource, Prompt } from "../config/schemas.ts";
|
|
3
|
-
import { getContext } from "../context.ts";
|
|
4
3
|
import { formatServerOverview, formatToolSchema, formatError } from "../output/formatter.ts";
|
|
5
|
-
import {
|
|
4
|
+
import { withCommand } from "./with-command.ts";
|
|
6
5
|
|
|
7
6
|
export function registerInfoCommand(program: Command) {
|
|
8
7
|
program
|
|
9
8
|
.command("info <server> [tool]")
|
|
10
9
|
.description("show server overview, or schema for a specific tool")
|
|
11
|
-
.action(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
.action(
|
|
11
|
+
withCommand(
|
|
12
|
+
program,
|
|
13
|
+
{ spinnerText: "Connecting..." },
|
|
14
|
+
async ({ manager, formatOptions, spinner }, server: string, tool?: string) => {
|
|
15
|
+
const target = tool ? `${server}/${tool}` : server;
|
|
16
|
+
spinner.update(`Connecting to ${target}...`);
|
|
17
|
+
|
|
18
|
+
if (tool) {
|
|
19
|
+
const toolSchema = await manager.getToolSchema(server, tool);
|
|
20
|
+
spinner.stop();
|
|
21
|
+
if (!toolSchema) {
|
|
22
|
+
console.error(
|
|
23
|
+
formatError(`Tool "${tool}" not found on server "${server}"`, formatOptions),
|
|
24
|
+
);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
console.log(formatToolSchema(server, toolSchema, formatOptions));
|
|
28
|
+
} else {
|
|
29
|
+
const serverInfo = await manager.getServerInfo(server);
|
|
30
|
+
const caps = serverInfo.capabilities as Record<string, unknown> | undefined;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const [tools, resources, prompts] = await Promise.all(fetches);
|
|
32
|
+
const fetches: [Promise<Tool[]>, Promise<Resource[]>, Promise<Prompt[]>] = [
|
|
33
|
+
caps?.tools !== undefined ? manager.listTools(server) : Promise.resolve([]),
|
|
34
|
+
caps?.resources !== undefined ? manager.listResources(server) : Promise.resolve([]),
|
|
35
|
+
caps?.prompts !== undefined ? manager.listPrompts(server) : Promise.resolve([]),
|
|
36
|
+
];
|
|
37
|
+
const [tools, resources, prompts] = await Promise.all(fetches);
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
process.exit(1);
|
|
59
|
-
} finally {
|
|
60
|
-
await manager.close();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
39
|
+
spinner.stop();
|
|
40
|
+
console.log(
|
|
41
|
+
formatServerOverview(
|
|
42
|
+
{
|
|
43
|
+
serverName: server,
|
|
44
|
+
version: serverInfo.version,
|
|
45
|
+
capabilities: caps,
|
|
46
|
+
instructions: serverInfo.instructions,
|
|
47
|
+
tools,
|
|
48
|
+
resourceCount: resources.length,
|
|
49
|
+
promptCount: prompts.length,
|
|
50
|
+
},
|
|
51
|
+
formatOptions,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
);
|
|
63
58
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -1,64 +1,59 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import {
|
|
3
|
-
import { formatUnifiedList, formatError } from "../output/formatter.ts";
|
|
2
|
+
import { formatUnifiedList } from "../output/formatter.ts";
|
|
4
3
|
import type { UnifiedItem } from "../output/formatter.ts";
|
|
5
|
-
import {
|
|
4
|
+
import { withCommand } from "./with-command.ts";
|
|
6
5
|
|
|
7
6
|
export function registerListCommand(program: Command) {
|
|
8
|
-
program.action(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
program.action(
|
|
8
|
+
withCommand(
|
|
9
|
+
program,
|
|
10
|
+
{ spinnerText: "Connecting to servers...", errorLabel: "Failed to list servers" },
|
|
11
|
+
async ({ manager, formatOptions, spinner }) => {
|
|
12
|
+
const [toolsResult, resourcesResult, promptsResult] = await Promise.all([
|
|
13
|
+
manager.getAllTools(),
|
|
14
|
+
manager.getAllResources(),
|
|
15
|
+
manager.getAllPrompts(),
|
|
16
|
+
]);
|
|
17
|
+
spinner.stop();
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
const items: UnifiedItem[] = [
|
|
20
|
+
...toolsResult.tools.map((t) => ({
|
|
21
|
+
server: t.server,
|
|
22
|
+
type: "tool" as const,
|
|
23
|
+
name: t.tool.name,
|
|
24
|
+
description: t.tool.description,
|
|
25
|
+
})),
|
|
26
|
+
...resourcesResult.resources.map((r) => ({
|
|
27
|
+
server: r.server,
|
|
28
|
+
type: "resource" as const,
|
|
29
|
+
name: r.resource.uri,
|
|
30
|
+
description: r.resource.description,
|
|
31
|
+
})),
|
|
32
|
+
...promptsResult.prompts.map((p) => ({
|
|
33
|
+
server: p.server,
|
|
34
|
+
type: "prompt" as const,
|
|
35
|
+
name: p.prompt.name,
|
|
36
|
+
description: p.prompt.description,
|
|
37
|
+
})),
|
|
38
|
+
];
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const typeOrder = { tool: 0, resource: 1, prompt: 2 };
|
|
41
|
+
items.sort((a, b) => {
|
|
42
|
+
if (a.server !== b.server) return a.server.localeCompare(b.server);
|
|
43
|
+
if (a.type !== b.type) return typeOrder[a.type] - typeOrder[b.type];
|
|
44
|
+
return a.name.localeCompare(b.name);
|
|
45
|
+
});
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const errors = [...toolsResult.errors, ...resourcesResult.errors, ...promptsResult.errors];
|
|
48
|
+
if (errors.length > 0) {
|
|
49
|
+
for (const err of errors) {
|
|
50
|
+
console.error(`"${err.server}": ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (items.length > 0) console.log("");
|
|
51
53
|
}
|
|
52
|
-
if (items.length > 0) console.log("");
|
|
53
|
-
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
process.exit(1);
|
|
60
|
-
} finally {
|
|
61
|
-
await manager.close();
|
|
62
|
-
}
|
|
63
|
-
});
|
|
55
|
+
console.log(formatUnifiedList(items, formatOptions));
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
);
|
|
64
59
|
}
|
package/src/commands/prompt.ts
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { getContext } from "../context.ts";
|
|
3
2
|
import {
|
|
4
3
|
formatPromptList,
|
|
5
4
|
formatServerPrompts,
|
|
6
5
|
formatPromptMessages,
|
|
7
6
|
formatError,
|
|
8
7
|
} from "../output/formatter.ts";
|
|
9
|
-
import { logger } from "../output/logger.ts";
|
|
10
8
|
import { parseJsonArgs, readStdin } from "../lib/input.ts";
|
|
9
|
+
import { withCommand } from "./with-command.ts";
|
|
11
10
|
|
|
12
11
|
export function registerPromptCommand(program: Command) {
|
|
13
12
|
program
|
|
14
13
|
.command("prompt [server] [name] [args]")
|
|
15
14
|
.description("list prompts for a server, or get a specific prompt")
|
|
16
15
|
.action(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
formatOptions,
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
withCommand(
|
|
17
|
+
program,
|
|
18
|
+
{ spinnerText: "Connecting to servers..." },
|
|
19
|
+
async (
|
|
20
|
+
{ manager, formatOptions, spinner },
|
|
21
|
+
server?: string,
|
|
22
|
+
name?: string,
|
|
23
|
+
argsStr?: string,
|
|
24
|
+
) => {
|
|
25
|
+
if (server) {
|
|
26
|
+
spinner.update(`Connecting to ${server}...`);
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
if (server && name) {
|
|
25
30
|
let args: Record<string, string> | undefined;
|
|
26
31
|
|
|
@@ -48,13 +53,7 @@ export function registerPromptCommand(program: Command) {
|
|
|
48
53
|
console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.error(formatError(String(err), formatOptions));
|
|
54
|
-
process.exit(1);
|
|
55
|
-
} finally {
|
|
56
|
-
await manager.close();
|
|
57
|
-
}
|
|
58
|
-
},
|
|
56
|
+
},
|
|
57
|
+
),
|
|
59
58
|
);
|
|
60
59
|
}
|
package/src/commands/resource.ts
CHANGED
|
@@ -1,46 +1,42 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { getContext } from "../context.ts";
|
|
3
2
|
import {
|
|
4
3
|
formatResourceList,
|
|
5
4
|
formatServerResources,
|
|
6
5
|
formatResourceContents,
|
|
7
6
|
formatError,
|
|
8
7
|
} from "../output/formatter.ts";
|
|
9
|
-
import {
|
|
8
|
+
import { withCommand } from "./with-command.ts";
|
|
10
9
|
|
|
11
10
|
export function registerResourceCommand(program: Command) {
|
|
12
11
|
program
|
|
13
12
|
.command("resource [server] [uri]")
|
|
14
13
|
.description("list resources for a server, or read a specific resource")
|
|
15
|
-
.action(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
formatOptions,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (server && uri) {
|
|
23
|
-
const result = await manager.readResource(server, uri);
|
|
24
|
-
spinner.stop();
|
|
25
|
-
console.log(formatResourceContents(server, uri, result, formatOptions));
|
|
26
|
-
} else if (server) {
|
|
27
|
-
const resources = await manager.listResources(server);
|
|
28
|
-
spinner.stop();
|
|
29
|
-
console.log(formatServerResources(server, resources, formatOptions));
|
|
30
|
-
} else {
|
|
31
|
-
const { resources, errors } = await manager.getAllResources();
|
|
32
|
-
spinner.stop();
|
|
33
|
-
console.log(formatResourceList(resources, formatOptions));
|
|
34
|
-
for (const err of errors) {
|
|
35
|
-
console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
|
|
14
|
+
.action(
|
|
15
|
+
withCommand(
|
|
16
|
+
program,
|
|
17
|
+
{ spinnerText: "Connecting to servers..." },
|
|
18
|
+
async ({ manager, formatOptions, spinner }, server?: string, uri?: string) => {
|
|
19
|
+
if (server) {
|
|
20
|
+
spinner.update(`Connecting to ${server}...`);
|
|
36
21
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
22
|
+
|
|
23
|
+
if (server && uri) {
|
|
24
|
+
const result = await manager.readResource(server, uri);
|
|
25
|
+
spinner.stop();
|
|
26
|
+
console.log(formatResourceContents(server, uri, result, formatOptions));
|
|
27
|
+
} else if (server) {
|
|
28
|
+
const resources = await manager.listResources(server);
|
|
29
|
+
spinner.stop();
|
|
30
|
+
console.log(formatServerResources(server, resources, formatOptions));
|
|
31
|
+
} else {
|
|
32
|
+
const { resources, errors } = await manager.getAllResources();
|
|
33
|
+
spinner.stop();
|
|
34
|
+
console.log(formatResourceList(resources, formatOptions));
|
|
35
|
+
for (const err of errors) {
|
|
36
|
+
console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
);
|
|
46
42
|
}
|
package/src/commands/search.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { search } from "../search/index.ts";
|
|
|
4
4
|
import { getStaleServers } from "../search/staleness.ts";
|
|
5
5
|
import { formatError, formatSearchResults } from "../output/formatter.ts";
|
|
6
6
|
import { logger } from "../output/logger.ts";
|
|
7
|
+
import { DEFAULTS } from "../constants.ts";
|
|
7
8
|
|
|
8
9
|
export function registerSearchCommand(program: Command) {
|
|
9
10
|
program
|
|
@@ -11,7 +12,7 @@ export function registerSearchCommand(program: Command) {
|
|
|
11
12
|
.description("search tools by keyword and/or semantic similarity")
|
|
12
13
|
.option("-k, --keyword", "keyword/glob search only")
|
|
13
14
|
.option("-q, --query", "semantic search only")
|
|
14
|
-
.option("-n, --limit <number>", "max results to return",
|
|
15
|
+
.option("-n, --limit <number>", "max results to return", String(DEFAULTS.SEARCH_TOP_K))
|
|
15
16
|
.action(
|
|
16
17
|
async (terms: string[], options: { keyword?: boolean; query?: boolean; limit: string }) => {
|
|
17
18
|
const query = terms.join(" ");
|
package/src/commands/servers.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { cyan, dim, green, yellow } from "ansis";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
|
-
import { getContext } from "../context.ts";
|
|
4
3
|
import { isStdioServer, isHttpServer } from "../config/schemas.ts";
|
|
5
|
-
import {
|
|
4
|
+
import { isInteractive } from "../output/formatter.ts";
|
|
5
|
+
import { withCommand } from "./with-command.ts";
|
|
6
6
|
|
|
7
7
|
export function registerServersCommand(program: Command) {
|
|
8
8
|
program
|
|
9
9
|
.command("servers")
|
|
10
10
|
.description("List configured MCP servers")
|
|
11
|
-
.action(
|
|
12
|
-
|
|
13
|
-
try {
|
|
11
|
+
.action(
|
|
12
|
+
withCommand(program, {}, async ({ config, formatOptions }) => {
|
|
14
13
|
const servers = Object.entries(config.servers.mcpServers);
|
|
15
14
|
|
|
16
15
|
if (!isInteractive(formatOptions)) {
|
|
@@ -56,11 +55,6 @@ export function registerServersCommand(program: Command) {
|
|
|
56
55
|
: dim(cfg.url);
|
|
57
56
|
console.log(`${n} ${type} ${detail}`);
|
|
58
57
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
process.exit(1);
|
|
62
|
-
} finally {
|
|
63
|
-
await manager.close();
|
|
64
|
-
}
|
|
65
|
-
});
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
66
60
|
}
|
package/src/commands/task.ts
CHANGED
|
@@ -1,82 +1,66 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { getContext } from "../context.ts";
|
|
3
2
|
import {
|
|
4
3
|
formatCallResult,
|
|
5
4
|
formatError,
|
|
6
5
|
formatTaskStatus,
|
|
7
6
|
formatTasksList,
|
|
8
7
|
} from "../output/formatter.ts";
|
|
9
|
-
import {
|
|
8
|
+
import { withCommand } from "./with-command.ts";
|
|
10
9
|
|
|
11
10
|
export function registerTaskCommand(program: Command) {
|
|
12
11
|
program
|
|
13
12
|
.command("task <action> <server> [taskId]")
|
|
14
13
|
.description("manage tasks (actions: get, list, result, cancel)")
|
|
15
|
-
.action(
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
.action(
|
|
15
|
+
withCommand(
|
|
16
|
+
program,
|
|
17
|
+
{ spinnerText: "Connecting..." },
|
|
18
|
+
async (
|
|
19
|
+
{ manager, formatOptions, spinner },
|
|
20
|
+
action: string,
|
|
21
|
+
server: string,
|
|
22
|
+
taskId?: string,
|
|
23
|
+
) => {
|
|
24
|
+
spinner.update(`Connecting to ${server}...`);
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
case "get": {
|
|
28
|
-
if (!taskId) {
|
|
29
|
-
spinner.error("Missing task ID");
|
|
30
|
-
console.error(formatError("Usage: mcpx task get <server> <taskId>", formatOptions));
|
|
31
|
-
process.exit(1);
|
|
26
|
+
switch (action) {
|
|
27
|
+
case "list": {
|
|
28
|
+
const result = await manager.listTasks(server);
|
|
29
|
+
spinner.stop();
|
|
30
|
+
console.log(formatTasksList(result.tasks, result.nextCursor, formatOptions));
|
|
31
|
+
break;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.error(
|
|
42
|
-
formatError("Usage: mcpx task result <server> <taskId>", formatOptions),
|
|
43
|
-
);
|
|
44
|
-
process.exit(1);
|
|
33
|
+
case "get": {
|
|
34
|
+
if (!taskId) {
|
|
35
|
+
throw new Error("Usage: mcpx task get <server> <taskId>");
|
|
36
|
+
}
|
|
37
|
+
const task = await manager.getTask(server, taskId);
|
|
38
|
+
spinner.stop();
|
|
39
|
+
console.log(formatTaskStatus(task, formatOptions));
|
|
40
|
+
break;
|
|
45
41
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
|
|
42
|
+
case "result": {
|
|
43
|
+
if (!taskId) {
|
|
44
|
+
throw new Error("Usage: mcpx task result <server> <taskId>");
|
|
45
|
+
}
|
|
46
|
+
const result = await manager.getTaskResult(server, taskId);
|
|
47
|
+
spinner.stop();
|
|
48
|
+
console.log(formatCallResult(result, formatOptions));
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "cancel": {
|
|
52
|
+
if (!taskId) {
|
|
53
|
+
throw new Error("Usage: mcpx task cancel <server> <taskId>");
|
|
54
|
+
}
|
|
55
|
+
const cancelled = await manager.cancelTask(server, taskId);
|
|
56
|
+
spinner.stop();
|
|
57
|
+
console.log(formatTaskStatus(cancelled, formatOptions));
|
|
58
|
+
break;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log(formatTaskStatus(cancelled, formatOptions));
|
|
62
|
-
break;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`Unknown task action: "${action}". Use: get, list, result, cancel`);
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
formatError(
|
|
68
|
-
`Unknown task action: "${action}". Use: get, list, result, cancel`,
|
|
69
|
-
formatOptions,
|
|
70
|
-
),
|
|
71
|
-
);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
} catch (err) {
|
|
75
|
-
spinner.error("Failed");
|
|
76
|
-
console.error(formatError(String(err), formatOptions));
|
|
77
|
-
process.exit(1);
|
|
78
|
-
} finally {
|
|
79
|
-
await manager.close();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
63
|
+
},
|
|
64
|
+
),
|
|
65
|
+
);
|
|
82
66
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { getContext, type AppContext } from "../context.ts";
|
|
3
|
+
import { formatError } from "../output/formatter.ts";
|
|
4
|
+
import { logger, type Spinner } from "../output/logger.ts";
|
|
5
|
+
|
|
6
|
+
export interface CommandContext extends AppContext {
|
|
7
|
+
spinner: Spinner;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface WithCommandOptions {
|
|
11
|
+
/** Spinner text shown during execution. If omitted, no spinner is started. */
|
|
12
|
+
spinnerText?: string;
|
|
13
|
+
/** Error message for spinner.error(). Defaults to "Failed". */
|
|
14
|
+
errorLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const noopSpinner: Spinner = {
|
|
18
|
+
update() {},
|
|
19
|
+
success() {},
|
|
20
|
+
error() {},
|
|
21
|
+
stop() {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wrap a command action with standard context setup, spinner, error handling,
|
|
26
|
+
* and manager cleanup.
|
|
27
|
+
*
|
|
28
|
+
* The handler receives { config, manager, formatOptions, spinner } and should:
|
|
29
|
+
* 1. Do async work
|
|
30
|
+
* 2. Call spinner.stop() when done
|
|
31
|
+
* 3. Output results via console.log()
|
|
32
|
+
*
|
|
33
|
+
* Errors are caught, formatted, and cause process.exit(1).
|
|
34
|
+
* manager.close() is always called in finally.
|
|
35
|
+
*/
|
|
36
|
+
export function withCommand<TArgs extends unknown[]>(
|
|
37
|
+
program: Command,
|
|
38
|
+
options: WithCommandOptions,
|
|
39
|
+
handler: (ctx: CommandContext, ...args: TArgs) => Promise<void>,
|
|
40
|
+
): (...args: TArgs) => Promise<void> {
|
|
41
|
+
return async (...args: TArgs) => {
|
|
42
|
+
const appCtx = await getContext(program);
|
|
43
|
+
const { manager, formatOptions } = appCtx;
|
|
44
|
+
|
|
45
|
+
const spinner = options.spinnerText
|
|
46
|
+
? logger.startSpinner(options.spinnerText, formatOptions)
|
|
47
|
+
: noopSpinner;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await handler({ ...appCtx, spinner }, ...args);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
spinner.error(options.errorLabel ?? "Failed");
|
|
53
|
+
console.error(formatError(String(err), formatOptions));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
} finally {
|
|
56
|
+
await manager.close();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/config/env.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { ENV } from "../constants.ts";
|
|
2
|
+
|
|
1
3
|
const ENV_VAR_PATTERN = /\$\{([^}]+)\}/g;
|
|
2
4
|
|
|
3
5
|
/** Whether to throw on missing env vars (default: true) */
|
|
4
6
|
function isStrictEnv(): boolean {
|
|
5
|
-
return process.env.
|
|
7
|
+
return process.env[ENV.STRICT_ENV] !== "false";
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
/** Replace ${VAR_NAME} in a string with the corresponding env var value */
|
|
@@ -12,7 +14,7 @@ export function interpolateEnvString(value: string): string {
|
|
|
12
14
|
if (envValue === undefined) {
|
|
13
15
|
if (isStrictEnv()) {
|
|
14
16
|
throw new Error(
|
|
15
|
-
`Environment variable "${varName}" is not set (set
|
|
17
|
+
`Environment variable "${varName}" is not set (set ${ENV.STRICT_ENV}=false to warn instead)`,
|
|
16
18
|
);
|
|
17
19
|
}
|
|
18
20
|
console.warn(`Warning: environment variable "${varName}" is not set`);
|