openclaw-cascade-plugin 1.0.12 → 1.0.14
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/grpc-client.d.ts +17 -0
- package/dist/grpc-client.d.ts.map +1 -0
- package/dist/grpc-client.js +154 -0
- package/dist/grpc-client.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -54
- package/dist/index.js.map +1 -1
- package/dist/test-utils/mocks.d.ts +6 -4
- package/dist/test-utils/mocks.d.ts.map +1 -1
- package/dist/test-utils/mocks.js +24 -14
- package/dist/test-utils/mocks.js.map +1 -1
- package/dist/tools/desktop-automation.d.ts +2 -7
- package/dist/tools/desktop-automation.d.ts.map +1 -1
- package/dist/tools/desktop-automation.js +64 -123
- package/dist/tools/desktop-automation.js.map +1 -1
- package/dist/tools/index.d.ts +3 -10
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -17
- package/dist/tools/index.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +13 -2
- package/proto/cascade.proto +297 -0
- package/PHASE1_SUMMARY.md +0 -191
- package/PHASE3_SUMMARY.md +0 -195
- package/dist/cascade-client.d.ts +0 -53
- package/dist/cascade-client.d.ts.map +0 -1
- package/dist/cascade-client.js +0 -179
- package/dist/cascade-client.js.map +0 -1
- package/dist/python-manager.d.ts +0 -59
- package/dist/python-manager.d.ts.map +0 -1
- package/dist/python-manager.js +0 -190
- package/dist/python-manager.js.map +0 -1
- package/dist/tools/api-tools.d.ts +0 -9
- package/dist/tools/api-tools.d.ts.map +0 -1
- package/dist/tools/api-tools.js +0 -102
- package/dist/tools/api-tools.js.map +0 -1
- package/dist/tools/sandbox-tools.d.ts +0 -9
- package/dist/tools/sandbox-tools.d.ts.map +0 -1
- package/dist/tools/sandbox-tools.js +0 -79
- package/dist/tools/sandbox-tools.js.map +0 -1
- package/dist/tools/web-automation.d.ts +0 -9
- package/dist/tools/web-automation.d.ts.map +0 -1
- package/dist/tools/web-automation.js +0 -471
- package/dist/tools/web-automation.js.map +0 -1
- package/jest.setup.js +0 -19
- package/openclaw-cascade-plugin-1.0.0.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.10.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.11.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.12.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.4.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.6.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.7.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.8.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.9.tgz +0 -0
- package/scripts/postinstall.js +0 -84
- package/src/a2a-client.ts +0 -66
- package/src/cascade-client.test.ts +0 -400
- package/src/cascade-client.ts +0 -198
- package/src/config.test.ts +0 -189
- package/src/config.ts +0 -137
- package/src/index.ts +0 -202
- package/src/python-manager.test.ts +0 -187
- package/src/python-manager.ts +0 -230
- package/src/test-utils/helpers.ts +0 -107
- package/src/test-utils/index.ts +0 -2
- package/src/test-utils/mocks.ts +0 -101
- package/src/tools/a2a-tools.ts +0 -162
- package/src/tools/api-tools.ts +0 -110
- package/src/tools/desktop-automation.test.ts +0 -308
- package/src/tools/desktop-automation.ts +0 -366
- package/src/tools/index.ts +0 -13
- package/src/tools/response-helpers.ts +0 -78
- package/src/tools/sandbox-tools.ts +0 -83
- package/src/tools/tool-registry.ts +0 -51
- package/src/tools/web-automation.test.ts +0 -177
- package/src/tools/web-automation.ts +0 -518
- package/src/types/index.ts +0 -133
- package/src/wsl.ts +0 -53
- package/tsconfig.json +0 -27
package/src/index.ts
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw Plugin Entry Point
|
|
3
|
-
*
|
|
4
|
-
* This is the main entry point for the @cascade/openclaw-plugin
|
|
5
|
-
* It initializes the plugin and registers all tools with OpenClaw
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { PythonManager } from './python-manager';
|
|
9
|
-
import { CascadeMcpClient } from './cascade-client';
|
|
10
|
-
import { CascadeA2AClient } from './a2a-client';
|
|
11
|
-
import { loadConfig } from './config';
|
|
12
|
-
import { CascadeError } from './types';
|
|
13
|
-
import { delimiter as pathDelimiter } from 'path';
|
|
14
|
-
import {
|
|
15
|
-
registerDesktopTools,
|
|
16
|
-
registerWebTools,
|
|
17
|
-
registerApiTools,
|
|
18
|
-
registerSandboxTools,
|
|
19
|
-
registerA2ATools
|
|
20
|
-
} from './tools';
|
|
21
|
-
import { ToolRegistry } from './tools/tool-registry';
|
|
22
|
-
|
|
23
|
-
// Placeholder for OpenClaw API type
|
|
24
|
-
interface OpenClawApi {
|
|
25
|
-
config: {
|
|
26
|
-
plugins: {
|
|
27
|
-
entries: {
|
|
28
|
-
cascade?: { config?: any };
|
|
29
|
-
'openclaw-cascade-plugin'?: { config?: any };
|
|
30
|
-
[key: string]: { config?: any } | undefined;
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
registerTool: (tool: any) => void;
|
|
35
|
-
registerGatewayMethod: (name: string, handler: Function) => void;
|
|
36
|
-
registerCli: (handler: Function) => void;
|
|
37
|
-
notify: (message: string) => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export default async function register(api: OpenClawApi) {
|
|
41
|
-
let mcpClient: CascadeMcpClient | null = null;
|
|
42
|
-
let a2aClient: CascadeA2AClient | null = null;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
// Load and validate configuration
|
|
46
|
-
const entries = api.config.plugins.entries || {};
|
|
47
|
-
const config = await loadConfig(
|
|
48
|
-
entries['openclaw-cascade-plugin']?.config ||
|
|
49
|
-
entries.cascade?.config ||
|
|
50
|
-
{}
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Lazy loaded clients
|
|
54
|
-
let pythonPath: string | null = null;
|
|
55
|
-
let initialized = false;
|
|
56
|
-
|
|
57
|
-
const getMcpClient = async (): Promise<CascadeMcpClient> => {
|
|
58
|
-
if (mcpClient && initialized) return mcpClient;
|
|
59
|
-
|
|
60
|
-
// Initialize Python Manager
|
|
61
|
-
const pythonManager = new PythonManager(config);
|
|
62
|
-
pythonPath = await pythonManager.findOrInstallPython();
|
|
63
|
-
|
|
64
|
-
// Initialize MCP Client
|
|
65
|
-
const pythonEnv: NodeJS.ProcessEnv = {
|
|
66
|
-
CASCADE_GRPC_ENDPOINT: config.cascadeGrpcEndpoint,
|
|
67
|
-
CASCADE_APP_ID: config.firestoreProjectId || 'openclaw',
|
|
68
|
-
CASCADE_USER_ID: 'openclaw-user',
|
|
69
|
-
...(config.firestoreCredentialsPath && {
|
|
70
|
-
GOOGLE_APPLICATION_CREDENTIALS: config.firestoreCredentialsPath
|
|
71
|
-
})
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
let modulePath = config.cascadePythonModulePath || process.env.CASCADE_PYTHON_MODULE_PATH;
|
|
75
|
-
|
|
76
|
-
// Auto-detect Cascade python module path relative to plugin installation
|
|
77
|
-
if (!modulePath) {
|
|
78
|
-
const { join } = require('path');
|
|
79
|
-
const { existsSync } = require('fs');
|
|
80
|
-
// __dirname is openclaw-plugin/dist
|
|
81
|
-
// repo root python is openclaw-plugin/../python
|
|
82
|
-
const guessedPath = join(__dirname, '..', '..', 'python');
|
|
83
|
-
if (existsSync(join(guessedPath, 'mcp_server', '__init__.py'))) {
|
|
84
|
-
modulePath = guessedPath;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (modulePath) {
|
|
89
|
-
const existingPath = process.env.PYTHONPATH || '';
|
|
90
|
-
pythonEnv.PYTHONPATH = existingPath
|
|
91
|
-
? `${modulePath}${pathDelimiter}${existingPath}`
|
|
92
|
-
: modulePath;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
mcpClient = new CascadeMcpClient(pythonPath, pythonEnv);
|
|
96
|
-
await mcpClient.start();
|
|
97
|
-
initialized = true;
|
|
98
|
-
return mcpClient;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const getA2aClient = async (): Promise<CascadeA2AClient | null> => {
|
|
102
|
-
if (!config.enableA2A) return null;
|
|
103
|
-
if (a2aClient) return a2aClient;
|
|
104
|
-
|
|
105
|
-
a2aClient = new CascadeA2AClient(
|
|
106
|
-
config.cascadeGrpcEndpoint,
|
|
107
|
-
'openclaw-user',
|
|
108
|
-
config.firestoreProjectId || 'openclaw',
|
|
109
|
-
'' // auth token would come from config
|
|
110
|
-
);
|
|
111
|
-
await a2aClient.initialize();
|
|
112
|
-
return a2aClient;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Create tool registry
|
|
116
|
-
const toolRegistry = new ToolRegistry();
|
|
117
|
-
|
|
118
|
-
// Register all tools using the getters
|
|
119
|
-
registerDesktopTools(toolRegistry, getMcpClient, config);
|
|
120
|
-
registerWebTools(toolRegistry, getMcpClient);
|
|
121
|
-
registerApiTools(toolRegistry, getMcpClient);
|
|
122
|
-
registerSandboxTools(toolRegistry, getMcpClient);
|
|
123
|
-
registerA2ATools(toolRegistry, getA2aClient);
|
|
124
|
-
|
|
125
|
-
// Register tools with OpenClaw
|
|
126
|
-
const tools = toolRegistry.getAll();
|
|
127
|
-
|
|
128
|
-
for (const tool of tools) {
|
|
129
|
-
// Ensure the schema is perfectly formatted for OpenAI and OpenClaw's internal validator
|
|
130
|
-
const schema = tool.inputSchema || { type: 'object', properties: {} };
|
|
131
|
-
|
|
132
|
-
api.registerTool({
|
|
133
|
-
name: tool.name,
|
|
134
|
-
description: tool.description,
|
|
135
|
-
schema: schema,
|
|
136
|
-
parameters: schema,
|
|
137
|
-
inputSchema: schema,
|
|
138
|
-
// Match the exact signature expected by OpenClaw's pi-agent-core AgentTool
|
|
139
|
-
execute: async (_toolCallId: string, params: any) => tool.handler(params),
|
|
140
|
-
handler: tool.handler // Fallback for legacy
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Register status check
|
|
145
|
-
api.registerGatewayMethod('cascade.status', () => ({
|
|
146
|
-
connected: mcpClient?.isConnected() || false,
|
|
147
|
-
toolsRegistered: tools.length,
|
|
148
|
-
pythonPath,
|
|
149
|
-
grpcEndpoint: config.cascadeGrpcEndpoint,
|
|
150
|
-
a2aEnabled: config.enableA2A
|
|
151
|
-
}));
|
|
152
|
-
|
|
153
|
-
// Register CLI command
|
|
154
|
-
api.registerCli(({ program }: { program: any }) => {
|
|
155
|
-
program
|
|
156
|
-
.command('cascade:status')
|
|
157
|
-
.description('Check Cascade plugin status')
|
|
158
|
-
.action(async () => {
|
|
159
|
-
let connected = false;
|
|
160
|
-
try {
|
|
161
|
-
// Only check if it's already initialized to avoid triggering init
|
|
162
|
-
if (initialized && mcpClient) {
|
|
163
|
-
connected = mcpClient.isConnected();
|
|
164
|
-
}
|
|
165
|
-
} catch(e) {}
|
|
166
|
-
|
|
167
|
-
console.log('Cascade Plugin Status:');
|
|
168
|
-
console.log(' Connected:', connected);
|
|
169
|
-
console.log(' Tools:', tools.length);
|
|
170
|
-
console.log(' Python:', pythonPath || 'Not yet initialized');
|
|
171
|
-
console.log(' gRPC:', config.cascadeGrpcEndpoint);
|
|
172
|
-
console.log(' A2A:', config.enableA2A ? 'enabled' : 'disabled');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
program
|
|
176
|
-
.command('cascade:tools')
|
|
177
|
-
.description('List all registered tools')
|
|
178
|
-
.action(() => {
|
|
179
|
-
console.log('Registered Tools:');
|
|
180
|
-
tools.forEach((tool, index) => {
|
|
181
|
-
console.log(` ${index + 1}. ${tool.name}`);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
} catch (error) {
|
|
187
|
-
console.error('Failed to initialize Cascade plugin:', error);
|
|
188
|
-
|
|
189
|
-
if (error instanceof CascadeError) {
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
throw new Error(
|
|
194
|
-
`Cascade plugin initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Export types for TypeScript users
|
|
200
|
-
export * from './types';
|
|
201
|
-
export { PythonManager, CascadeMcpClient, CascadeA2AClient, loadConfig };
|
|
202
|
-
// Note: ToolRegistry is internal use only - not exported to avoid conflicts
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for PythonManager
|
|
3
|
-
*
|
|
4
|
-
* Test-driven development: Write tests first, then implement
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { PythonManager } from './python-manager';
|
|
8
|
-
import { createMockConfig } from './test-utils';
|
|
9
|
-
|
|
10
|
-
describe('PythonManager', () => {
|
|
11
|
-
let pythonManager: PythonManager;
|
|
12
|
-
let mockExec: jest.Mock;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
jest.clearAllMocks();
|
|
16
|
-
mockExec = jest.fn();
|
|
17
|
-
|
|
18
|
-
// Mock the exec method directly on the module
|
|
19
|
-
jest.doMock('child_process', () => ({
|
|
20
|
-
exec: mockExec
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
pythonManager = new PythonManager(createMockConfig());
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
jest.dontMock('child_process');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('findOrInstallPython', () => {
|
|
31
|
-
test('should return configured path when cascadePythonPath is set', async () => {
|
|
32
|
-
// Arrange
|
|
33
|
-
const config = createMockConfig({ cascadePythonPath: '/custom/python' });
|
|
34
|
-
pythonManager = new PythonManager(config);
|
|
35
|
-
|
|
36
|
-
// Mock the execAsync method directly
|
|
37
|
-
const execSpy = jest.spyOn(pythonManager as any, 'execAsync')
|
|
38
|
-
.mockResolvedValue({ stdout: 'Python 3.12.0', stderr: '' });
|
|
39
|
-
|
|
40
|
-
// Act
|
|
41
|
-
const result = await pythonManager.findOrInstallPython();
|
|
42
|
-
|
|
43
|
-
// Assert
|
|
44
|
-
expect(result).toBe('/custom/python');
|
|
45
|
-
expect(execSpy).toHaveBeenCalledWith('/custom/python --version');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('should auto-detect python3.12 in common locations', async () => {
|
|
49
|
-
// Arrange
|
|
50
|
-
let callCount = 0;
|
|
51
|
-
const execSpy = jest.spyOn(pythonManager as any, 'execAsync')
|
|
52
|
-
.mockImplementation(() => {
|
|
53
|
-
callCount++;
|
|
54
|
-
if (callCount <= 2) {
|
|
55
|
-
return Promise.reject(new Error('not found'));
|
|
56
|
-
}
|
|
57
|
-
return Promise.resolve({ stdout: 'Python 3.10.0', stderr: '' });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Act
|
|
61
|
-
const result = await pythonManager.findOrInstallPython();
|
|
62
|
-
|
|
63
|
-
// Assert
|
|
64
|
-
expect(result).toBe('python3.10');
|
|
65
|
-
expect(execSpy).toHaveBeenCalledWith('python3.12 --version');
|
|
66
|
-
expect(execSpy).toHaveBeenCalledWith('python3.11 --version');
|
|
67
|
-
expect(execSpy).toHaveBeenCalledWith('python3.10 --version');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('should fallback through version candidates in order', async () => {
|
|
71
|
-
// Arrange
|
|
72
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
73
|
-
.mockResolvedValue({ stdout: 'Python 3.12.1', stderr: '' });
|
|
74
|
-
|
|
75
|
-
// Act
|
|
76
|
-
const result = await pythonManager.findOrInstallPython();
|
|
77
|
-
|
|
78
|
-
// Assert
|
|
79
|
-
expect(result).toBe('python3.12');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('should throw error when no Python found and auto-install fails', async () => {
|
|
83
|
-
// Arrange
|
|
84
|
-
jest.spyOn(pythonManager as any, 'execAsync').mockRejectedValue(new Error('not found'));
|
|
85
|
-
|
|
86
|
-
// Mock install to fail
|
|
87
|
-
jest.spyOn(pythonManager as any, 'installPython').mockRejectedValue(
|
|
88
|
-
new Error('Installation failed')
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// Act & Assert
|
|
92
|
-
await expect(pythonManager.findOrInstallPython()).rejects.toThrow('Installation failed');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('isValidPython', () => {
|
|
97
|
-
test('should return true for Python 3.10+', async () => {
|
|
98
|
-
// Arrange
|
|
99
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
100
|
-
.mockResolvedValue({ stdout: 'Python 3.12.0', stderr: '' });
|
|
101
|
-
|
|
102
|
-
// Act
|
|
103
|
-
const result = await (pythonManager as any).isValidPython('python3');
|
|
104
|
-
|
|
105
|
-
// Assert
|
|
106
|
-
expect(result).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('should return true for Python 3.10.x', async () => {
|
|
110
|
-
// Arrange
|
|
111
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
112
|
-
.mockResolvedValue({ stdout: 'Python 3.10.5', stderr: '' });
|
|
113
|
-
|
|
114
|
-
// Act
|
|
115
|
-
const result = await (pythonManager as any).isValidPython('python3');
|
|
116
|
-
|
|
117
|
-
// Assert
|
|
118
|
-
expect(result).toBe(true);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('should return false for Python 3.9', async () => {
|
|
122
|
-
// Arrange
|
|
123
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
124
|
-
.mockResolvedValue({ stdout: 'Python 3.9.7', stderr: '' });
|
|
125
|
-
|
|
126
|
-
// Act
|
|
127
|
-
const result = await (pythonManager as any).isValidPython('python3');
|
|
128
|
-
|
|
129
|
-
// Assert
|
|
130
|
-
expect(result).toBe(false);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('should return false for Python 2.x', async () => {
|
|
134
|
-
// Arrange
|
|
135
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
136
|
-
.mockResolvedValue({ stdout: 'Python 2.7.18', stderr: '' });
|
|
137
|
-
|
|
138
|
-
// Act
|
|
139
|
-
const result = await (pythonManager as any).isValidPython('python');
|
|
140
|
-
|
|
141
|
-
// Assert
|
|
142
|
-
expect(result).toBe(false);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('should return false for non-Python executables', async () => {
|
|
146
|
-
// Arrange
|
|
147
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
148
|
-
.mockResolvedValue({ stdout: 'not python output', stderr: '' });
|
|
149
|
-
|
|
150
|
-
// Act
|
|
151
|
-
const result = await (pythonManager as any).isValidPython('notpython');
|
|
152
|
-
|
|
153
|
-
// Assert
|
|
154
|
-
expect(result).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('should return false when command not found', async () => {
|
|
158
|
-
// Arrange
|
|
159
|
-
jest.spyOn(pythonManager as any, 'execAsync')
|
|
160
|
-
.mockRejectedValue(new Error('command not found'));
|
|
161
|
-
|
|
162
|
-
// Act
|
|
163
|
-
const result = await (pythonManager as any).isValidPython('fakepython');
|
|
164
|
-
|
|
165
|
-
// Assert
|
|
166
|
-
expect(result).toBe(false);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('parsePythonVersion', () => {
|
|
171
|
-
test('should parse standard version output', () => {
|
|
172
|
-
// Act & Assert
|
|
173
|
-
expect((pythonManager as any).parsePythonVersion('Python 3.12.0')).toBe(3.12);
|
|
174
|
-
expect((pythonManager as any).parsePythonVersion('Python 3.10.5')).toBe(3.10);
|
|
175
|
-
expect((pythonManager as any).parsePythonVersion('Python 3.9.0')).toBe(3.09);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('should handle version without patch', () => {
|
|
179
|
-
expect((pythonManager as any).parsePythonVersion('Python 3.12')).toBe(3.12);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('should return 0 for invalid output', () => {
|
|
183
|
-
expect((pythonManager as any).parsePythonVersion('not python')).toBe(0);
|
|
184
|
-
expect((pythonManager as any).parsePythonVersion('')).toBe(0);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
});
|
package/src/python-manager.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python Manager - Handles Python environment detection and installation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { exec } from 'child_process';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
import { CascadePluginConfig } from './types';
|
|
8
|
-
|
|
9
|
-
export class PythonManager {
|
|
10
|
-
private readonly MIN_PYTHON_VERSION = 3.10;
|
|
11
|
-
|
|
12
|
-
constructor(private config: CascadePluginConfig) {}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Find or install Python for Cascade
|
|
16
|
-
* Priority: 1) Configured path 2) Auto-detect 3) Auto-install
|
|
17
|
-
*/
|
|
18
|
-
async findOrInstallPython(): Promise<string> {
|
|
19
|
-
// 1. Check configured path
|
|
20
|
-
if (this.config.cascadePythonPath) {
|
|
21
|
-
if (await this.isValidPython(this.config.cascadePythonPath)) {
|
|
22
|
-
return this.config.cascadePythonPath;
|
|
23
|
-
}
|
|
24
|
-
console.warn(`Configured Python path ${this.config.cascadePythonPath} is not valid, searching...`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 2. Auto-detect in common locations
|
|
28
|
-
const detected = await this.autoDetectPython();
|
|
29
|
-
if (detected) {
|
|
30
|
-
return detected;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 3. Auto-install
|
|
34
|
-
console.log('Python not found. Installing...');
|
|
35
|
-
return this.installPython();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Auto-detect Python in common locations
|
|
40
|
-
*/
|
|
41
|
-
private async autoDetectPython(): Promise<string | null> {
|
|
42
|
-
const candidates = this.getPythonCandidates();
|
|
43
|
-
|
|
44
|
-
for (const cmd of candidates) {
|
|
45
|
-
if (await this.isValidPython(cmd)) {
|
|
46
|
-
return cmd;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Get list of Python commands to try
|
|
55
|
-
*/
|
|
56
|
-
private getPythonCandidates(): string[] {
|
|
57
|
-
const candidates = [
|
|
58
|
-
'python3.12',
|
|
59
|
-
'python3.11',
|
|
60
|
-
'python3.10',
|
|
61
|
-
'python3',
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
if (process.platform === 'win32') {
|
|
65
|
-
// Windows-specific paths
|
|
66
|
-
const localAppData = process.env.LOCALAPPDATA || '';
|
|
67
|
-
const programFiles = process.env.ProgramFiles || '';
|
|
68
|
-
|
|
69
|
-
candidates.push(
|
|
70
|
-
join(localAppData, 'Programs', 'Python', 'Python312', 'python.exe'),
|
|
71
|
-
join(localAppData, 'Programs', 'Python', 'Python311', 'python.exe'),
|
|
72
|
-
join(localAppData, 'Programs', 'Python', 'Python310', 'python.exe'),
|
|
73
|
-
join(programFiles, 'Python312', 'python.exe'),
|
|
74
|
-
join(programFiles, 'Python311', 'python.exe'),
|
|
75
|
-
join(programFiles, 'Python310', 'python.exe'),
|
|
76
|
-
'python.exe',
|
|
77
|
-
'python'
|
|
78
|
-
);
|
|
79
|
-
} else {
|
|
80
|
-
// Unix-like paths
|
|
81
|
-
candidates.push(
|
|
82
|
-
'/usr/bin/python3.12',
|
|
83
|
-
'/usr/bin/python3.11',
|
|
84
|
-
'/usr/bin/python3.10',
|
|
85
|
-
'/usr/bin/python3',
|
|
86
|
-
'/usr/local/bin/python3.12',
|
|
87
|
-
'/usr/local/bin/python3.11',
|
|
88
|
-
'/usr/local/bin/python3.10',
|
|
89
|
-
'/usr/local/bin/python3',
|
|
90
|
-
'/opt/homebrew/bin/python3.12',
|
|
91
|
-
'/opt/homebrew/bin/python3.11',
|
|
92
|
-
'/opt/homebrew/bin/python3.10',
|
|
93
|
-
'/opt/homebrew/bin/python3'
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return candidates;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Check if Python command is valid and meets version requirements
|
|
102
|
-
*/
|
|
103
|
-
async isValidPython(cmd: string): Promise<boolean> {
|
|
104
|
-
try {
|
|
105
|
-
const { stdout } = await this.execAsync(`${cmd} --version`);
|
|
106
|
-
const version = this.parsePythonVersion(stdout);
|
|
107
|
-
return version >= this.MIN_PYTHON_VERSION;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Parse Python version from version string
|
|
115
|
-
*/
|
|
116
|
-
parsePythonVersion(versionOutput: string): number {
|
|
117
|
-
const match = versionOutput.match(/Python (\d+)\.(\d+)/);
|
|
118
|
-
if (!match) return 0;
|
|
119
|
-
|
|
120
|
-
const major = parseInt(match[1], 10);
|
|
121
|
-
const minor = parseInt(match[2], 10);
|
|
122
|
-
return major + minor / 100;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Execute command and return stdout
|
|
127
|
-
*/
|
|
128
|
-
private execAsync(command: string): Promise<{ stdout: string; stderr: string }> {
|
|
129
|
-
return new Promise((resolve, reject) => {
|
|
130
|
-
exec(command, (error, stdout, stderr) => {
|
|
131
|
-
if (error) {
|
|
132
|
-
reject(error);
|
|
133
|
-
} else {
|
|
134
|
-
resolve({ stdout, stderr });
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Auto-install Python based on platform
|
|
142
|
-
*/
|
|
143
|
-
private async installPython(): Promise<string> {
|
|
144
|
-
if (process.platform === 'win32') {
|
|
145
|
-
return this.installPythonWindows();
|
|
146
|
-
} else if (process.platform === 'darwin') {
|
|
147
|
-
return this.installPythonMacOS();
|
|
148
|
-
} else {
|
|
149
|
-
return this.installPythonLinux();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Install Python on Windows
|
|
155
|
-
*/
|
|
156
|
-
private async installPythonWindows(): Promise<string> {
|
|
157
|
-
try {
|
|
158
|
-
const installerPath = await this.downloadPythonInstaller();
|
|
159
|
-
await this.runPythonInstaller(installerPath);
|
|
160
|
-
return 'python';
|
|
161
|
-
} catch (error) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
`Failed to install Python on Windows: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
|
|
164
|
-
'Please install Python 3.10+ manually from https://python.org'
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Download Python installer for Windows
|
|
171
|
-
*/
|
|
172
|
-
private async downloadPythonInstaller(): Promise<string> {
|
|
173
|
-
// In production, this would download from python.org
|
|
174
|
-
// For now, we assume the user needs to install manually
|
|
175
|
-
throw new Error('Automatic installation not implemented. Please install Python manually.');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Run Python installer on Windows
|
|
180
|
-
*/
|
|
181
|
-
private async runPythonInstaller(installerPath: string): Promise<void> {
|
|
182
|
-
await this.execAsync(`"${installerPath}" /quiet InstallAllUsers=0 PrependPath=1`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Install Python on macOS using Homebrew
|
|
187
|
-
*/
|
|
188
|
-
private async installPythonMacOS(): Promise<string> {
|
|
189
|
-
// Check if brew is available
|
|
190
|
-
try {
|
|
191
|
-
await this.execAsync('which brew');
|
|
192
|
-
} catch {
|
|
193
|
-
throw new Error(
|
|
194
|
-
'Homebrew not found. Please install Homebrew first: https://brew.sh'
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
await this.execAsync('brew install python@3.12');
|
|
200
|
-
return '/usr/local/bin/python3.12';
|
|
201
|
-
} catch (error) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`Failed to install Python via Homebrew: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Install Python on Linux using apt
|
|
210
|
-
*/
|
|
211
|
-
private async installPythonLinux(): Promise<string> {
|
|
212
|
-
// Check if apt is available
|
|
213
|
-
try {
|
|
214
|
-
await this.execAsync('which apt');
|
|
215
|
-
} catch {
|
|
216
|
-
throw new Error(
|
|
217
|
-
'apt not found. Please install Python 3.10+ manually using your package manager.'
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
await this.execAsync('sudo apt-get update && sudo apt-get install -y python3.12 python3.12-venv');
|
|
223
|
-
return '/usr/bin/python3.12';
|
|
224
|
-
} catch (error) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`Failed to install Python via apt: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|