@toolplex/client 0.1.18 → 0.1.19
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/dist/mcp-server/index.js +10 -0
- package/dist/mcp-server/registry.d.ts +17 -0
- package/dist/mcp-server/registry.js +23 -0
- package/dist/mcp-server/toolHandlers/initHandler.d.ts +9 -0
- package/dist/mcp-server/toolHandlers/initHandler.js +46 -1
- package/dist/mcp-server/toolplexServer.js +5 -0
- package/dist/mcp-server/utils/runtimeCheck.d.ts +18 -0
- package/dist/mcp-server/utils/runtimeCheck.js +70 -6
- package/dist/server-manager/serverManager.js +3 -9
- package/dist/shared/enhancedPath.js +3 -3
- package/dist/shared/mcpServerTypes.d.ts +12 -0
- package/dist/src/mcp-server/clientContext.js +118 -0
- package/dist/src/mcp-server/logging/telemetryLogger.js +54 -0
- package/dist/src/mcp-server/policy/callToolObserver.js +27 -0
- package/dist/src/mcp-server/policy/feedbackPolicy.js +39 -0
- package/dist/src/mcp-server/policy/installObserver.js +35 -0
- package/dist/src/mcp-server/policy/playbookPolicy.js +81 -0
- package/dist/src/mcp-server/policy/policyEnforcer.js +105 -0
- package/dist/src/mcp-server/policy/serverPolicy.js +63 -0
- package/dist/src/mcp-server/promptsCache.js +51 -0
- package/dist/src/mcp-server/registry.js +134 -0
- package/dist/src/mcp-server/serversCache.js +100 -0
- package/dist/src/mcp-server/toolDefinitionsCache.js +67 -0
- package/dist/src/mcp-server/toolHandlers/initHandler.js +185 -0
- package/dist/src/mcp-server/toolplexApi/service.js +221 -0
- package/dist/src/mcp-server/toolplexApi/types.js +1 -0
- package/dist/src/mcp-server/utils/initServerManagers.js +31 -0
- package/dist/src/mcp-server/utils/runtimeCheck.js +94 -0
- package/dist/src/shared/enhancedPath.js +52 -0
- package/dist/src/shared/fileLogger.js +66 -0
- package/dist/src/shared/mcpServerTypes.js +158 -0
- package/dist/src/shared/serverManagerTypes.js +73 -0
- package/dist/src/shared/stdioServerManagerClient.js +98 -0
- package/dist/tests/unit/bundledDependencies.test.js +152 -0
- package/dist/tests/unit/registry.test.js +216 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/types/types.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// --------------------
|
|
4
|
+
// Enums
|
|
5
|
+
// --------------------
|
|
6
|
+
export const TransportTypeSchema = z.enum(["stdio", "sse"]);
|
|
7
|
+
export const RuntimeSchema = z.enum(["node", "python", "go", "docker"]);
|
|
8
|
+
// --------------------
|
|
9
|
+
// LLMContext
|
|
10
|
+
// --------------------
|
|
11
|
+
export const LLMContextSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
model_family: z.string(),
|
|
14
|
+
model_name: z.string().min(1),
|
|
15
|
+
model_version: z.string(),
|
|
16
|
+
chat_client: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
// --------------------
|
|
20
|
+
// ServerConfig
|
|
21
|
+
// --------------------
|
|
22
|
+
export const ServerConfigSchema = z.object({
|
|
23
|
+
server_name: z.string().optional(),
|
|
24
|
+
description: z.string().optional(),
|
|
25
|
+
command: z.string().optional(),
|
|
26
|
+
args: z.array(z.string()).optional(),
|
|
27
|
+
runtime: RuntimeSchema.optional(),
|
|
28
|
+
env: z.record(z.string()).optional(),
|
|
29
|
+
url: z.string().optional(),
|
|
30
|
+
transport: TransportTypeSchema,
|
|
31
|
+
});
|
|
32
|
+
// --------------------
|
|
33
|
+
// InitializeToolplexParams
|
|
34
|
+
// --------------------
|
|
35
|
+
export const InitializeToolplexParamsSchema = z.object({
|
|
36
|
+
llm_context: LLMContextSchema,
|
|
37
|
+
});
|
|
38
|
+
// --------------------
|
|
39
|
+
// SearchParams
|
|
40
|
+
// --------------------
|
|
41
|
+
export const SearchParamsSchema = z.object({
|
|
42
|
+
query: z.string(),
|
|
43
|
+
expanded_keywords: z.array(z.string()).optional(),
|
|
44
|
+
filter: z.enum(["all", "servers_only", "playbooks_only"]).optional(),
|
|
45
|
+
size: z.number().int().min(1).max(25).optional(),
|
|
46
|
+
scope: z.enum(["all", "public_only", "private_only"]).optional(),
|
|
47
|
+
});
|
|
48
|
+
// --------------------
|
|
49
|
+
// LookupEntityParams
|
|
50
|
+
// --------------------
|
|
51
|
+
export const LookupEntityParamsSchema = z.object({
|
|
52
|
+
entity_type: z.enum(["server", "playbook", "feedback"]),
|
|
53
|
+
entity_id: z.string(),
|
|
54
|
+
});
|
|
55
|
+
// --------------------
|
|
56
|
+
// InstallParams
|
|
57
|
+
// --------------------
|
|
58
|
+
export const InstallParamsSchema = z.object({
|
|
59
|
+
server_id: z.string(),
|
|
60
|
+
server_name: z.string(),
|
|
61
|
+
description: z.string(),
|
|
62
|
+
config: ServerConfigSchema,
|
|
63
|
+
});
|
|
64
|
+
// --------------------
|
|
65
|
+
// ListToolsParams
|
|
66
|
+
// --------------------
|
|
67
|
+
export const ListToolsParamsSchema = z.object({
|
|
68
|
+
server_id: z.string().optional(),
|
|
69
|
+
});
|
|
70
|
+
// --------------------
|
|
71
|
+
// GetServerConfigParams
|
|
72
|
+
// --------------------
|
|
73
|
+
export const GetServerConfigParamsSchema = z.object({
|
|
74
|
+
server_id: z.string().optional(),
|
|
75
|
+
});
|
|
76
|
+
// --------------------
|
|
77
|
+
// CallToolParams
|
|
78
|
+
// --------------------
|
|
79
|
+
export const CallToolParamsSchema = z.object({
|
|
80
|
+
server_id: z.string(),
|
|
81
|
+
tool_name: z.string(),
|
|
82
|
+
arguments: z.record(z.any()),
|
|
83
|
+
});
|
|
84
|
+
// --------------------
|
|
85
|
+
// UninstallParams
|
|
86
|
+
// --------------------
|
|
87
|
+
export const UninstallParamsSchema = z.object({
|
|
88
|
+
server_id: z.string(),
|
|
89
|
+
});
|
|
90
|
+
// --------------------
|
|
91
|
+
// PlaybookAction (shared type for actions)
|
|
92
|
+
// --------------------
|
|
93
|
+
export const PlaybookActionSchema = z.object({
|
|
94
|
+
do: z.string(),
|
|
95
|
+
call: z.string().optional(),
|
|
96
|
+
args: z
|
|
97
|
+
.record(z.object({
|
|
98
|
+
type: z.enum([
|
|
99
|
+
"string",
|
|
100
|
+
"number",
|
|
101
|
+
"boolean",
|
|
102
|
+
"array",
|
|
103
|
+
"object",
|
|
104
|
+
"placeholder",
|
|
105
|
+
]),
|
|
106
|
+
example: z.any(),
|
|
107
|
+
}))
|
|
108
|
+
.optional(),
|
|
109
|
+
});
|
|
110
|
+
// --------------------
|
|
111
|
+
// SavePlaybookParams
|
|
112
|
+
// --------------------
|
|
113
|
+
export const SavePlaybookParamsSchema = z.object({
|
|
114
|
+
playbook_name: z.string(),
|
|
115
|
+
description: z.string(),
|
|
116
|
+
// Requires at least one action to have a "call" property
|
|
117
|
+
actions: z
|
|
118
|
+
.array(PlaybookActionSchema)
|
|
119
|
+
.refine((actions) => actions.some((action) => typeof action.call === "string" && action.call.length > 0), {
|
|
120
|
+
message: 'At least one action must include a "call" property',
|
|
121
|
+
path: ["actions"],
|
|
122
|
+
}),
|
|
123
|
+
domain: z.string().optional(),
|
|
124
|
+
keywords: z.array(z.string()).optional(),
|
|
125
|
+
requirements: z.array(z.string()).optional(),
|
|
126
|
+
privacy: z.enum(["public", "private"]).optional(),
|
|
127
|
+
source_playbook_id: z.string().optional(),
|
|
128
|
+
fork_reason: z.string().optional(),
|
|
129
|
+
});
|
|
130
|
+
// --------------------
|
|
131
|
+
// LogPlaybookUsageParams
|
|
132
|
+
// --------------------
|
|
133
|
+
export const LogPlaybookUsageParamsSchema = z.object({
|
|
134
|
+
playbook_id: z.string(),
|
|
135
|
+
success: z.boolean(),
|
|
136
|
+
error_message: z.string().optional(),
|
|
137
|
+
});
|
|
138
|
+
// --------------------
|
|
139
|
+
// SubmitFeedbackParams
|
|
140
|
+
// --------------------
|
|
141
|
+
export const SubmitFeedbackParamsSchema = z.object({
|
|
142
|
+
target_type: z.enum(["server", "playbook"]),
|
|
143
|
+
target_id: z.string(),
|
|
144
|
+
vote: z.enum(["up", "down"]),
|
|
145
|
+
message: z.string().optional(),
|
|
146
|
+
security_assessment: z
|
|
147
|
+
.object({
|
|
148
|
+
security_flags: z.array(z.union([
|
|
149
|
+
z.string(),
|
|
150
|
+
z.object({
|
|
151
|
+
custom_flag: z.string(),
|
|
152
|
+
}),
|
|
153
|
+
])),
|
|
154
|
+
risk_assessment: z.string(),
|
|
155
|
+
context_note: z.string().optional(),
|
|
156
|
+
})
|
|
157
|
+
.optional(),
|
|
158
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// --------------------
|
|
3
|
+
// initialize
|
|
4
|
+
// --------------------
|
|
5
|
+
export const InitializeResultSchema = z.object({
|
|
6
|
+
succeeded: z.array(z.object({
|
|
7
|
+
server_id: z.string(),
|
|
8
|
+
server_name: z.string(),
|
|
9
|
+
description: z.string(),
|
|
10
|
+
})),
|
|
11
|
+
failures: z.record(z.object({
|
|
12
|
+
server_id: z.string(),
|
|
13
|
+
server_name: z.string(),
|
|
14
|
+
error: z.string(),
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
// --------------------
|
|
18
|
+
// Install
|
|
19
|
+
// --------------------
|
|
20
|
+
export const ServerInstallResultSchema = z.object({
|
|
21
|
+
server_id: z.string(),
|
|
22
|
+
server_name: z.string(),
|
|
23
|
+
});
|
|
24
|
+
// --------------------
|
|
25
|
+
// Uninstall
|
|
26
|
+
// --------------------
|
|
27
|
+
export const ServerUninstallResultSchema = z.object({
|
|
28
|
+
server_id: z.string(),
|
|
29
|
+
server_name: z.string(),
|
|
30
|
+
});
|
|
31
|
+
// --------------------
|
|
32
|
+
// list_servers
|
|
33
|
+
// --------------------
|
|
34
|
+
export const ListServersResultSchema = z.object({
|
|
35
|
+
servers: z.array(z.object({
|
|
36
|
+
server_id: z.string(),
|
|
37
|
+
server_name: z.string(),
|
|
38
|
+
description: z.string(),
|
|
39
|
+
tool_count: z.number(),
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
// --------------------
|
|
43
|
+
// list_tools
|
|
44
|
+
// --------------------
|
|
45
|
+
export const ListToolsResultSchema = z.object({
|
|
46
|
+
server_id: z.string(),
|
|
47
|
+
server_name: z.string(),
|
|
48
|
+
tools: z.array(z.object({
|
|
49
|
+
name: z.string(),
|
|
50
|
+
description: z.string().optional(),
|
|
51
|
+
inputSchema: z.any(),
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
// --------------------
|
|
55
|
+
// list_all_tools
|
|
56
|
+
// --------------------
|
|
57
|
+
export const ListAllToolsResultSchema = z.object({
|
|
58
|
+
tools: z.record(z.array(z.object({
|
|
59
|
+
name: z.string(),
|
|
60
|
+
description: z.string().optional(),
|
|
61
|
+
inputSchema: z.any(),
|
|
62
|
+
}))),
|
|
63
|
+
});
|
|
64
|
+
// --------------------
|
|
65
|
+
// call_tool
|
|
66
|
+
// --------------------
|
|
67
|
+
export const CallToolResultSchema = z.object({
|
|
68
|
+
result: z.any(),
|
|
69
|
+
});
|
|
70
|
+
// --------------------
|
|
71
|
+
// cleanup
|
|
72
|
+
// --------------------
|
|
73
|
+
export const CleanupResultSchema = z.object({});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
export class StdioServerManagerClient {
|
|
3
|
+
constructor(command, args, env) {
|
|
4
|
+
this.serverProcess = null;
|
|
5
|
+
this.messageHandlers = new Map();
|
|
6
|
+
this.messageBuffer = "";
|
|
7
|
+
this.command = command;
|
|
8
|
+
this.args = args;
|
|
9
|
+
this.env = env ? { ...process.env, ...env } : process.env;
|
|
10
|
+
}
|
|
11
|
+
async start() {
|
|
12
|
+
this.serverProcess = spawn(this.command, this.args, {
|
|
13
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
+
env: this.env,
|
|
15
|
+
});
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
this.serverProcess.on("error", (err) => {
|
|
18
|
+
process.stderr.write(`Server process error: ${err}\n`);
|
|
19
|
+
});
|
|
20
|
+
if (!this.serverProcess.stderr ||
|
|
21
|
+
!this.serverProcess.stdout ||
|
|
22
|
+
!this.serverProcess.stdin) {
|
|
23
|
+
throw new Error("Server process streams not available");
|
|
24
|
+
}
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
this.serverProcess.stderr.on("data", (data) => {
|
|
27
|
+
process.stderr.write(data);
|
|
28
|
+
});
|
|
29
|
+
this.serverProcess.stdout.on("data", (data) => {
|
|
30
|
+
this.messageBuffer += data.toString();
|
|
31
|
+
const lines = this.messageBuffer.split("\n");
|
|
32
|
+
this.messageBuffer = lines.pop() || "";
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (!line.trim())
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const message = JSON.parse(line.trim());
|
|
38
|
+
const handler = this.messageHandlers.get(message.id);
|
|
39
|
+
if (handler) {
|
|
40
|
+
handler(message);
|
|
41
|
+
this.messageHandlers.delete(message.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
process.stderr.write(`Raw line: ${line}\n`);
|
|
46
|
+
process.stderr.write(`Parse error: ${e}\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.serverProcess.on("exit", () => {
|
|
51
|
+
for (const [, h] of this.messageHandlers)
|
|
52
|
+
h({ __error__: { message: "Subprocess exited" } });
|
|
53
|
+
this.messageHandlers.clear();
|
|
54
|
+
this.serverProcess = null;
|
|
55
|
+
this.messageBuffer = "";
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
async sendRequest(method, params) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const id = Date.now();
|
|
62
|
+
if (!this.serverProcess?.stdin) {
|
|
63
|
+
reject(Error("Server process not started"));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.messageHandlers.set(id, (frame) => {
|
|
67
|
+
if (frame.error || frame.__error__) {
|
|
68
|
+
const msg = frame.error?.message ?? frame.__error__?.message ?? "MCP error";
|
|
69
|
+
reject(new Error(msg));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
resolve(frame.result);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
this.serverProcess.stdin.write(JSON.stringify({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
method,
|
|
78
|
+
params,
|
|
79
|
+
id,
|
|
80
|
+
}) + "\n");
|
|
81
|
+
}
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
if (this.messageHandlers.has(id)) {
|
|
84
|
+
this.messageHandlers.delete(id);
|
|
85
|
+
reject(new Error(`Request timed out: ${method}`));
|
|
86
|
+
}
|
|
87
|
+
}, 60000);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async stop() {
|
|
91
|
+
if (this.serverProcess) {
|
|
92
|
+
this.serverProcess.kill();
|
|
93
|
+
this.serverProcess = null;
|
|
94
|
+
this.messageHandlers.clear();
|
|
95
|
+
this.messageBuffer = "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Bundled Dependencies feature
|
|
3
|
+
*
|
|
4
|
+
* Tests both RuntimeCheck.resolveDependency() and initHandler.resolveDependencyForInit()
|
|
5
|
+
* which implement the bundled > system > error priority logic.
|
|
6
|
+
*
|
|
7
|
+
* Note: These tests use the real fs and which modules but control the test environment
|
|
8
|
+
* through Registry state and test file fixtures.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, beforeEach } from "node:test";
|
|
11
|
+
import assert from "node:assert";
|
|
12
|
+
import { RuntimeCheck } from "../../src/mcp-server/utils/runtimeCheck.js";
|
|
13
|
+
import { resolveDependencyForInit } from "../../src/mcp-server/toolHandlers/initHandler.js";
|
|
14
|
+
import Registry from "../../src/mcp-server/registry.js";
|
|
15
|
+
describe("Bundled Dependencies", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset Registry before each test
|
|
18
|
+
Registry.reset();
|
|
19
|
+
});
|
|
20
|
+
describe("RuntimeCheck.resolveDependency", () => {
|
|
21
|
+
describe("Priority 2: System PATH Fallback (using real system)", () => {
|
|
22
|
+
it("should resolve node from system PATH when no bundled deps", () => {
|
|
23
|
+
Registry.setBundledDependencies({});
|
|
24
|
+
// node should be available in system PATH
|
|
25
|
+
const resolved = RuntimeCheck.resolveDependency("node");
|
|
26
|
+
assert.ok(resolved.includes("node"));
|
|
27
|
+
assert.ok(resolved.length > 0);
|
|
28
|
+
});
|
|
29
|
+
it("should handle non-bundled dependencies", () => {
|
|
30
|
+
Registry.setBundledDependencies({ node: "/bundled/node" });
|
|
31
|
+
// git should fallback to system PATH (assuming git is installed)
|
|
32
|
+
try {
|
|
33
|
+
const resolved = RuntimeCheck.resolveDependency("git");
|
|
34
|
+
assert.ok(resolved.includes("git"));
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// If git is not installed, that's okay - this test just verifies the logic
|
|
38
|
+
assert.ok(err.message.includes("git"));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("Priority 3: Error When Neither Available", () => {
|
|
43
|
+
it("should throw error with hints for unknown commands", () => {
|
|
44
|
+
Registry.setBundledDependencies({});
|
|
45
|
+
assert.throws(() => RuntimeCheck.resolveDependency("definitely_not_a_real_command_12345"), /Command.*not found/);
|
|
46
|
+
});
|
|
47
|
+
it("should include install hints for known commands", () => {
|
|
48
|
+
Registry.setBundledDependencies({});
|
|
49
|
+
try {
|
|
50
|
+
// Try to resolve uvx which likely doesn't exist
|
|
51
|
+
RuntimeCheck.resolveDependency("uvx");
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// If it throws, it should have a helpful message
|
|
55
|
+
assert.ok(err.message.includes("uvx") ||
|
|
56
|
+
err.message.includes("Install") ||
|
|
57
|
+
err.message.includes("not found"));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("resolveDependencyForInit", () => {
|
|
63
|
+
describe("Priority 2: System PATH Fallback (using real system)", () => {
|
|
64
|
+
it("should return system path when bundled not available", () => {
|
|
65
|
+
const result = resolveDependencyForInit(undefined, "node");
|
|
66
|
+
// Should find node in system PATH and mark it as (system)
|
|
67
|
+
assert.ok(result.includes("(system)"));
|
|
68
|
+
assert.ok(result.includes("node"));
|
|
69
|
+
});
|
|
70
|
+
it("should handle undefined bundled path", () => {
|
|
71
|
+
const result = resolveDependencyForInit(undefined, "node");
|
|
72
|
+
// Should fallback gracefully
|
|
73
|
+
assert.ok(typeof result === "string");
|
|
74
|
+
assert.ok(result.length > 0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("Priority 3: Not Available", () => {
|
|
78
|
+
it("should return 'not available' for non-existent commands", () => {
|
|
79
|
+
const result = resolveDependencyForInit(undefined, "definitely_not_a_real_command_12345");
|
|
80
|
+
assert.strictEqual(result, "not available");
|
|
81
|
+
});
|
|
82
|
+
it("should return 'not available' for non-existent bundled path", () => {
|
|
83
|
+
const result = resolveDependencyForInit("/this/path/definitely/does/not/exist/node", "node");
|
|
84
|
+
// Since bundled path doesn't exist, should either find system or return not available
|
|
85
|
+
assert.ok(result.includes("(system)") || result === "not available");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("RuntimeCheck Utilities", () => {
|
|
90
|
+
describe("extractCommandName", () => {
|
|
91
|
+
it("should extract first word as command name", () => {
|
|
92
|
+
assert.strictEqual(RuntimeCheck.extractCommandName("node"), "node");
|
|
93
|
+
assert.strictEqual(RuntimeCheck.extractCommandName("node --version"), "node");
|
|
94
|
+
assert.strictEqual(RuntimeCheck.extractCommandName("npx -y @some/package"), "npx");
|
|
95
|
+
});
|
|
96
|
+
it("should handle leading/trailing spaces", () => {
|
|
97
|
+
assert.strictEqual(RuntimeCheck.extractCommandName(" node "), "node");
|
|
98
|
+
assert.strictEqual(RuntimeCheck.extractCommandName(" python3 -m module "), "python3");
|
|
99
|
+
});
|
|
100
|
+
it("should handle empty strings", () => {
|
|
101
|
+
assert.strictEqual(RuntimeCheck.extractCommandName(""), "");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe("validateCommandOrThrow", () => {
|
|
105
|
+
it("should not throw for available commands like node", () => {
|
|
106
|
+
// node should be available in system PATH
|
|
107
|
+
assert.doesNotThrow(() => {
|
|
108
|
+
RuntimeCheck.validateCommandOrThrow("node");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
it("should throw for unavailable commands", () => {
|
|
112
|
+
assert.throws(() => {
|
|
113
|
+
RuntimeCheck.validateCommandOrThrow("definitely_not_a_real_command_12345");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
it("should extract command name from full command string", () => {
|
|
117
|
+
// Should extract 'node' and validate it exists
|
|
118
|
+
assert.doesNotThrow(() => {
|
|
119
|
+
RuntimeCheck.validateCommandOrThrow("node --version");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("Registry Integration", () => {
|
|
125
|
+
it("should return copy of bundled dependencies", () => {
|
|
126
|
+
const deps = {
|
|
127
|
+
node: "/bundled/node",
|
|
128
|
+
python: "/bundled/python3",
|
|
129
|
+
};
|
|
130
|
+
Registry.setBundledDependencies(deps);
|
|
131
|
+
const retrieved = Registry.getBundledDependencies();
|
|
132
|
+
// Should be equal but not the same reference
|
|
133
|
+
assert.deepStrictEqual(retrieved, deps);
|
|
134
|
+
assert.notStrictEqual(retrieved, deps);
|
|
135
|
+
});
|
|
136
|
+
it("should handle bundled dependency path lookups", () => {
|
|
137
|
+
Registry.setBundledDependencies({
|
|
138
|
+
node: "/bundled/node",
|
|
139
|
+
python: "/bundled/python3",
|
|
140
|
+
});
|
|
141
|
+
assert.strictEqual(Registry.getBundledDependencyPath("node"), "/bundled/node");
|
|
142
|
+
assert.strictEqual(Registry.getBundledDependencyPath("python"), "/bundled/python3");
|
|
143
|
+
assert.strictEqual(Registry.getBundledDependencyPath("git"), undefined);
|
|
144
|
+
});
|
|
145
|
+
it("should reset bundled dependencies", () => {
|
|
146
|
+
Registry.setBundledDependencies({ node: "/bundled/node" });
|
|
147
|
+
Registry.reset();
|
|
148
|
+
const deps = Registry.getBundledDependencies();
|
|
149
|
+
assert.deepStrictEqual(deps, {});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|