@kite-copilot/cli 1.0.0 → 1.0.2
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/index.js +132 -128
- package/dist/remote-analyzer.d.ts +30 -0
- package/dist/remote-analyzer.js +431 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ const readline = __importStar(require("node:readline"));
|
|
|
52
52
|
const config_js_1 = require("./config.js");
|
|
53
53
|
const login_js_1 = require("./login.js");
|
|
54
54
|
const analyzer_js_1 = require("./analyzer.js");
|
|
55
|
+
const remote_analyzer_js_1 = require("./remote-analyzer.js");
|
|
55
56
|
const program = new commander_1.Command();
|
|
56
57
|
/**
|
|
57
58
|
* Prompt user for yes/no confirmation
|
|
@@ -69,6 +70,111 @@ async function promptYesNo(question) {
|
|
|
69
70
|
});
|
|
70
71
|
});
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Handle analysis result - display, prompt, and save/upload
|
|
75
|
+
* Shared between local and remote analysis
|
|
76
|
+
*/
|
|
77
|
+
async function handleAnalysisResult(result, options, quiet, verbose, spinner, skipPrompt) {
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(chalk_1.default.gray(`[DEBUG] Found ${result.routes.length} routes`));
|
|
80
|
+
console.log(chalk_1.default.gray(`[DEBUG] Found ${result.api_calls.length} API calls`));
|
|
81
|
+
console.log(chalk_1.default.gray(`[DEBUG] Generated ${result.mappings.length} mappings`));
|
|
82
|
+
}
|
|
83
|
+
// Display results
|
|
84
|
+
if (!quiet) {
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk_1.default.bold("Analysis Results"));
|
|
87
|
+
console.log(chalk_1.default.gray("-".repeat(40)));
|
|
88
|
+
// Routes
|
|
89
|
+
console.log(chalk_1.default.cyan(`\nRoutes (${result.routes.length}):`));
|
|
90
|
+
for (const route of result.routes) {
|
|
91
|
+
console.log(` - ${route.route_name} -> ${route.component_name}`);
|
|
92
|
+
console.log(chalk_1.default.gray(` ${route.file_path}`));
|
|
93
|
+
}
|
|
94
|
+
// API Calls
|
|
95
|
+
console.log(chalk_1.default.cyan(`\nAPI Calls (${result.api_calls.length}):`));
|
|
96
|
+
for (const call of result.api_calls) {
|
|
97
|
+
console.log(` - ${call.method} ${call.endpoint}`);
|
|
98
|
+
console.log(chalk_1.default.gray(` ${call.component_file}`));
|
|
99
|
+
}
|
|
100
|
+
// Mappings
|
|
101
|
+
console.log(chalk_1.default.cyan(`\nMappings (${result.mappings.length}):`));
|
|
102
|
+
for (const mapping of result.mappings) {
|
|
103
|
+
console.log(` - ${mapping.tool_name} -> ${mapping.api_endpoint}`);
|
|
104
|
+
if (mapping.navigation_page) {
|
|
105
|
+
console.log(chalk_1.default.gray(` Page: ${mapping.navigation_page}`));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log();
|
|
109
|
+
}
|
|
110
|
+
// Summary
|
|
111
|
+
if (!quiet) {
|
|
112
|
+
console.log();
|
|
113
|
+
console.log(chalk_1.default.bold("Summary"));
|
|
114
|
+
console.log(chalk_1.default.gray("-".repeat(40)));
|
|
115
|
+
console.log(` Routes: ${result.routes.length}`);
|
|
116
|
+
console.log(` API Calls: ${result.api_calls.length}`);
|
|
117
|
+
console.log(` Mappings: ${result.mappings.length}`);
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
// Handle save decision
|
|
121
|
+
if (skipPrompt) {
|
|
122
|
+
// --no-prompt: save locally
|
|
123
|
+
const outputPath = options.output || "kite-mappings.json";
|
|
124
|
+
(0, analyzer_js_1.saveResultToFile)(result, outputPath);
|
|
125
|
+
if (!quiet) {
|
|
126
|
+
console.log(chalk_1.default.green(`Saved mappings to: ${outputPath}`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Prompt user for confirmation
|
|
131
|
+
console.log();
|
|
132
|
+
const shouldUpload = await promptYesNo(chalk_1.default.cyan("Save mappings to Kite? (y/n): "));
|
|
133
|
+
if (shouldUpload) {
|
|
134
|
+
// Check if logged in
|
|
135
|
+
if (!(0, config_js_1.isLoggedIn)()) {
|
|
136
|
+
console.log(chalk_1.default.yellow("\nNot logged in. Run 'kite login' first to upload mappings."));
|
|
137
|
+
// Save to rejected as fallback
|
|
138
|
+
const rejectedPath = (0, config_js_1.saveRejectedMapping)(result);
|
|
139
|
+
console.log(chalk_1.default.gray(` Saved locally to: ${rejectedPath}\n`));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const config = (0, config_js_1.loadConfig)();
|
|
143
|
+
if (!quiet) {
|
|
144
|
+
spinner.start("Uploading mappings to Kite...");
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const uploadResult = await (0, analyzer_js_1.uploadMappings)(result, {
|
|
148
|
+
backendUrl: config.backend_url,
|
|
149
|
+
token: config.token,
|
|
150
|
+
orgId: config.org_id,
|
|
151
|
+
apiConfigId: config.api_config_id,
|
|
152
|
+
});
|
|
153
|
+
if (!quiet) {
|
|
154
|
+
spinner.succeed(`Uploaded ${uploadResult.mappings_uploaded} mappings ` +
|
|
155
|
+
`(merged: ${uploadResult.mappings_merged}, skipped: ${uploadResult.mappings_skipped})`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (uploadError) {
|
|
159
|
+
if (!quiet)
|
|
160
|
+
spinner.fail("Upload failed");
|
|
161
|
+
console.log(chalk_1.default.red(`\n${uploadError.message}`));
|
|
162
|
+
// Save to rejected on upload failure
|
|
163
|
+
const rejectedPath = (0, config_js_1.saveRejectedMapping)(result);
|
|
164
|
+
console.log(chalk_1.default.gray(` Saved locally to: ${rejectedPath}\n`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// User declined - save to rejected folder
|
|
170
|
+
const rejectedPath = (0, config_js_1.saveRejectedMapping)(result);
|
|
171
|
+
if (!quiet) {
|
|
172
|
+
console.log(chalk_1.default.yellow(`\nMappings saved locally to: ${rejectedPath}`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
console.log();
|
|
177
|
+
}
|
|
72
178
|
program
|
|
73
179
|
.name("kite")
|
|
74
180
|
.description("Kite CLI - Analyze frontend codebases and map API calls")
|
|
@@ -98,159 +204,57 @@ program
|
|
|
98
204
|
.option("-v, --verbose", "Show verbose debug output")
|
|
99
205
|
.option("--timeout <seconds>", "Analysis timeout in seconds", "300")
|
|
100
206
|
.option("--no-prompt", "Skip confirmation prompt and save locally")
|
|
207
|
+
.option("--api-endpoint <url>", "Override default API endpoint (for development)")
|
|
208
|
+
.option("--ssh-key <path>", "SSH private key for remote server authentication")
|
|
101
209
|
.action(async (codebasePath, options) => {
|
|
102
210
|
const quiet = options.quiet || false;
|
|
103
211
|
const verbose = options.verbose || false;
|
|
104
212
|
const skipPrompt = options.prompt === false;
|
|
105
|
-
|
|
213
|
+
const apiEndpoint = options.apiEndpoint || process.env.KITE_API_ENDPOINT;
|
|
106
214
|
const spinner = (0, ora_1.default)();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!quiet)
|
|
113
|
-
spinner.fail("Claude CLI not found");
|
|
114
|
-
console.log(chalk_1.default.red("\n✗ Claude CLI is required for analysis.\n"));
|
|
115
|
-
console.log(chalk_1.default.gray(" Install it with: npm install -g @anthropic-ai/claude-cli\n"));
|
|
215
|
+
// Verify remote analysis is configured
|
|
216
|
+
const remoteAvailable = (0, remote_analyzer_js_1.isRemoteAnalysisAvailable)() || !!apiEndpoint;
|
|
217
|
+
if (!remoteAvailable) {
|
|
218
|
+
console.log(chalk_1.default.red("\n✗ Remote analysis not available.\n"));
|
|
219
|
+
console.log(chalk_1.default.gray(" Unable to connect to analysis server.\n"));
|
|
116
220
|
process.exit(1);
|
|
117
221
|
}
|
|
118
|
-
|
|
119
|
-
spinner.succeed("Claude CLI found");
|
|
120
|
-
// Run analysis
|
|
222
|
+
// Remote analysis mode (only mode available)
|
|
121
223
|
if (!quiet) {
|
|
122
|
-
spinner.start(
|
|
224
|
+
spinner.start("Preparing remote analysis...");
|
|
123
225
|
}
|
|
124
226
|
if (verbose) {
|
|
227
|
+
console.log(chalk_1.default.gray(`[DEBUG] Using remote analysis`));
|
|
228
|
+
console.log(chalk_1.default.gray(`[DEBUG] API endpoint: ${apiEndpoint || "(from env)"}`));
|
|
125
229
|
console.log(chalk_1.default.gray(`[DEBUG] Path: ${codebasePath}`));
|
|
126
230
|
console.log(chalk_1.default.gray(`[DEBUG] Timeout: ${options.timeout}s`));
|
|
127
231
|
}
|
|
128
232
|
try {
|
|
129
|
-
const result = await (0,
|
|
233
|
+
const result = await (0, remote_analyzer_js_1.analyzeWithRemoteClaude)(codebasePath, {
|
|
130
234
|
timeout: parseInt(options.timeout, 10),
|
|
131
235
|
verbose,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (verbose) {
|
|
137
|
-
console.log(chalk_1.default.gray(`[DEBUG] Found ${result.routes.length} routes`));
|
|
138
|
-
console.log(chalk_1.default.gray(`[DEBUG] Found ${result.api_calls.length} API calls`));
|
|
139
|
-
console.log(chalk_1.default.gray(`[DEBUG] Generated ${result.mappings.length} mappings`));
|
|
140
|
-
}
|
|
141
|
-
// Display results
|
|
142
|
-
if (!quiet) {
|
|
143
|
-
console.log();
|
|
144
|
-
console.log(chalk_1.default.bold("📊 Analysis Results"));
|
|
145
|
-
console.log(chalk_1.default.gray("─".repeat(40)));
|
|
146
|
-
// Routes
|
|
147
|
-
console.log(chalk_1.default.cyan(`\nRoutes (${result.routes.length}):`));
|
|
148
|
-
for (const route of result.routes) {
|
|
149
|
-
console.log(` • ${route.route_name} → ${route.component_name}`);
|
|
150
|
-
console.log(chalk_1.default.gray(` ${route.file_path}`));
|
|
151
|
-
}
|
|
152
|
-
// API Calls
|
|
153
|
-
console.log(chalk_1.default.cyan(`\nAPI Calls (${result.api_calls.length}):`));
|
|
154
|
-
for (const call of result.api_calls) {
|
|
155
|
-
console.log(` • ${call.method} ${call.endpoint}`);
|
|
156
|
-
console.log(chalk_1.default.gray(` ${call.component_file}`));
|
|
157
|
-
}
|
|
158
|
-
// Mappings
|
|
159
|
-
console.log(chalk_1.default.cyan(`\nMappings (${result.mappings.length}):`));
|
|
160
|
-
for (const mapping of result.mappings) {
|
|
161
|
-
console.log(` • ${mapping.tool_name} → ${mapping.api_endpoint}`);
|
|
162
|
-
if (mapping.navigation_page) {
|
|
163
|
-
console.log(chalk_1.default.gray(` Page: ${mapping.navigation_page}`));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
console.log();
|
|
167
|
-
}
|
|
168
|
-
// Summary
|
|
169
|
-
if (!quiet) {
|
|
170
|
-
console.log();
|
|
171
|
-
console.log(chalk_1.default.bold("Summary"));
|
|
172
|
-
console.log(chalk_1.default.gray("─".repeat(40)));
|
|
173
|
-
console.log(` Routes: ${result.routes.length}`);
|
|
174
|
-
console.log(` API Calls: ${result.api_calls.length}`);
|
|
175
|
-
console.log(` Mappings: ${result.mappings.length}`);
|
|
176
|
-
console.log();
|
|
177
|
-
}
|
|
178
|
-
// Handle save decision
|
|
179
|
-
if (skipPrompt) {
|
|
180
|
-
// --no-prompt: save locally
|
|
181
|
-
const outputPath = options.output || "kite-mappings.json";
|
|
182
|
-
(0, analyzer_js_1.saveResultToFile)(result, outputPath);
|
|
236
|
+
apiEndpoint,
|
|
237
|
+
sshKeyPath: options.sshKey,
|
|
238
|
+
}, (message) => {
|
|
183
239
|
if (!quiet) {
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
// Prompt user for confirmation
|
|
189
|
-
console.log();
|
|
190
|
-
const shouldUpload = await promptYesNo(chalk_1.default.cyan("Save mappings to Kite? (y/n): "));
|
|
191
|
-
if (shouldUpload) {
|
|
192
|
-
// Check if logged in
|
|
193
|
-
if (!(0, config_js_1.isLoggedIn)()) {
|
|
194
|
-
console.log(chalk_1.default.yellow("\n⚠ Not logged in. Run 'kite login' first to upload mappings."));
|
|
195
|
-
// Save to rejected as fallback
|
|
196
|
-
const rejectedPath = (0, config_js_1.saveRejectedMapping)(result);
|
|
197
|
-
console.log(chalk_1.default.gray(` Saved locally to: ${rejectedPath}\n`));
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
const config = (0, config_js_1.loadConfig)();
|
|
201
|
-
if (!quiet) {
|
|
202
|
-
spinner.start("Uploading mappings to Kite...");
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
const uploadResult = await (0, analyzer_js_1.uploadMappings)(result, {
|
|
206
|
-
backendUrl: config.backend_url,
|
|
207
|
-
token: config.token,
|
|
208
|
-
orgId: config.org_id,
|
|
209
|
-
apiConfigId: config.api_config_id,
|
|
210
|
-
});
|
|
211
|
-
if (!quiet) {
|
|
212
|
-
spinner.succeed(`Uploaded ${uploadResult.mappings_uploaded} mappings ` +
|
|
213
|
-
`(merged: ${uploadResult.mappings_merged}, skipped: ${uploadResult.mappings_skipped})`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
catch (uploadError) {
|
|
217
|
-
if (!quiet)
|
|
218
|
-
spinner.fail("Upload failed");
|
|
219
|
-
console.log(chalk_1.default.red(`\n✗ ${uploadError.message}`));
|
|
220
|
-
// Save to rejected on upload failure
|
|
221
|
-
const rejectedPath = (0, config_js_1.saveRejectedMapping)(result);
|
|
222
|
-
console.log(chalk_1.default.gray(` Saved locally to: ${rejectedPath}\n`));
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
// User declined - save to rejected folder
|
|
228
|
-
const rejectedPath = (0, config_js_1.saveRejectedMapping)(result);
|
|
229
|
-
if (!quiet) {
|
|
230
|
-
console.log(chalk_1.default.yellow(`\n✓ Mappings saved locally to: ${rejectedPath}`));
|
|
231
|
-
}
|
|
240
|
+
spinner.text = message;
|
|
232
241
|
}
|
|
242
|
+
});
|
|
243
|
+
if (!quiet) {
|
|
244
|
+
spinner.succeed("Remote analysis complete");
|
|
233
245
|
}
|
|
234
|
-
|
|
246
|
+
await handleAnalysisResult(result, options, quiet, verbose, spinner, skipPrompt);
|
|
235
247
|
}
|
|
236
248
|
catch (error) {
|
|
237
249
|
if (!quiet)
|
|
238
250
|
spinner.fail("Analysis failed");
|
|
239
|
-
if (error instanceof
|
|
240
|
-
console.log(chalk_1.default.red(`\n✗ ${error.message}\n`));
|
|
241
|
-
console.log(chalk_1.default.gray(" Install Claude CLI: npm install -g @anthropic-ai/claude-cli\n"));
|
|
242
|
-
}
|
|
243
|
-
else if (error instanceof analyzer_js_1.ClaudeTimeoutError) {
|
|
244
|
-
console.log(chalk_1.default.red(`\n✗ ${error.message}\n`));
|
|
245
|
-
console.log(chalk_1.default.gray(" Try increasing the timeout with --timeout\n"));
|
|
251
|
+
if (error instanceof remote_analyzer_js_1.EC2StartError) {
|
|
252
|
+
console.log(chalk_1.default.red(`\n✗ Failed to start remote server: ${error.message}\n`));
|
|
246
253
|
}
|
|
247
|
-
else if (error instanceof
|
|
248
|
-
console.log(chalk_1.default.red(`\n✗
|
|
249
|
-
if (verbose) {
|
|
250
|
-
console.log(chalk_1.default.gray(` ${error.message}\n`));
|
|
251
|
-
}
|
|
254
|
+
else if (error instanceof remote_analyzer_js_1.SSHTunnelError) {
|
|
255
|
+
console.log(chalk_1.default.red(`\n✗ SSH connection failed: ${error.message}\n`));
|
|
252
256
|
}
|
|
253
|
-
else if (error instanceof
|
|
257
|
+
else if (error instanceof remote_analyzer_js_1.RemoteAnalyzerError) {
|
|
254
258
|
console.log(chalk_1.default.red(`\n✗ ${error.message}\n`));
|
|
255
259
|
}
|
|
256
260
|
else {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Claude Code Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Runs Claude Code analysis on a remote EC2 instance via SSH tunnel.
|
|
5
|
+
* The codebase stays on the user's machine - only accessed via SSHFS.
|
|
6
|
+
*/
|
|
7
|
+
import type { FrontendAnalysisResult, AnalyzeOptions } from "./types.js";
|
|
8
|
+
export declare class RemoteAnalyzerError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class EC2StartError extends RemoteAnalyzerError {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class SSHTunnelError extends RemoteAnalyzerError {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Main entry point: Analyze codebase using remote Claude Code
|
|
19
|
+
*/
|
|
20
|
+
export declare function analyzeWithRemoteClaude(codebasePath: string, options?: AnalyzeOptions & {
|
|
21
|
+
apiEndpoint?: string;
|
|
22
|
+
}, onProgress?: (message: string) => void): Promise<FrontendAnalysisResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Check if remote analysis is available (API endpoint configured)
|
|
25
|
+
*/
|
|
26
|
+
export declare function isRemoteAnalysisAvailable(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Get the configured API endpoint
|
|
29
|
+
*/
|
|
30
|
+
export declare function getApiEndpoint(): string;
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Remote Claude Code Analyzer
|
|
4
|
+
*
|
|
5
|
+
* Runs Claude Code analysis on a remote EC2 instance via SSH tunnel.
|
|
6
|
+
* The codebase stays on the user's machine - only accessed via SSHFS.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.SSHTunnelError = exports.EC2StartError = exports.RemoteAnalyzerError = void 0;
|
|
43
|
+
exports.analyzeWithRemoteClaude = analyzeWithRemoteClaude;
|
|
44
|
+
exports.isRemoteAnalysisAvailable = isRemoteAnalysisAvailable;
|
|
45
|
+
exports.getApiEndpoint = getApiEndpoint;
|
|
46
|
+
const node_child_process_1 = require("node:child_process");
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const os = __importStar(require("node:os"));
|
|
50
|
+
const net = __importStar(require("node:net"));
|
|
51
|
+
const analyzer_js_1 = require("./analyzer.js");
|
|
52
|
+
// Production API Gateway endpoint (env var override for development only)
|
|
53
|
+
const DEFAULT_API_ENDPOINT = process.env.KITE_API_ENDPOINT || "https://shmxocfur1.execute-api.us-east-1.amazonaws.com";
|
|
54
|
+
class RemoteAnalyzerError extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "RemoteAnalyzerError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.RemoteAnalyzerError = RemoteAnalyzerError;
|
|
61
|
+
class EC2StartError extends RemoteAnalyzerError {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "EC2StartError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.EC2StartError = EC2StartError;
|
|
68
|
+
class SSHTunnelError extends RemoteAnalyzerError {
|
|
69
|
+
constructor(message) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "SSHTunnelError";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.SSHTunnelError = SSHTunnelError;
|
|
75
|
+
/**
|
|
76
|
+
* Find an available local port for SSH tunnel
|
|
77
|
+
*/
|
|
78
|
+
async function findAvailablePort() {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const server = net.createServer();
|
|
81
|
+
server.listen(0, "127.0.0.1", () => {
|
|
82
|
+
const address = server.address();
|
|
83
|
+
if (address && typeof address === "object") {
|
|
84
|
+
const port = address.port;
|
|
85
|
+
server.close(() => resolve(port));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
reject(new Error("Could not get port"));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
server.on("error", reject);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Generate a temporary SSH keypair for this session
|
|
96
|
+
*/
|
|
97
|
+
async function generateTempSSHKey() {
|
|
98
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "kite-ssh-"));
|
|
99
|
+
const privateKeyPath = path.join(tempDir, "id_ed25519");
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const proc = (0, node_child_process_1.spawn)("ssh-keygen", [
|
|
102
|
+
"-t", "ed25519",
|
|
103
|
+
"-f", privateKeyPath,
|
|
104
|
+
"-N", "", // No passphrase
|
|
105
|
+
"-q", // Quiet
|
|
106
|
+
]);
|
|
107
|
+
proc.on("close", (code) => {
|
|
108
|
+
if (code !== 0) {
|
|
109
|
+
reject(new SSHTunnelError(`Failed to generate SSH key: exit code ${code}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const publicKey = fs.readFileSync(`${privateKeyPath}.pub`, "utf-8").trim();
|
|
113
|
+
resolve({ privateKeyPath, publicKey });
|
|
114
|
+
});
|
|
115
|
+
proc.on("error", (err) => {
|
|
116
|
+
reject(new SSHTunnelError(`Failed to generate SSH key: ${err.message}`));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Start the EC2 instance via API Gateway
|
|
122
|
+
*/
|
|
123
|
+
async function startEC2Instance(apiEndpoint) {
|
|
124
|
+
const response = await fetch(`${apiEndpoint}/instance`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ action: "start" }),
|
|
128
|
+
});
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
throw new EC2StartError(`Failed to start EC2: ${response.status} ${text}`);
|
|
132
|
+
}
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
if (!data.public_ip) {
|
|
135
|
+
throw new EC2StartError("EC2 started but no public IP available");
|
|
136
|
+
}
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get EC2 instance status
|
|
141
|
+
*/
|
|
142
|
+
async function getEC2Status(apiEndpoint) {
|
|
143
|
+
const response = await fetch(`${apiEndpoint}/instance?action=status`);
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
const text = await response.text();
|
|
146
|
+
throw new EC2StartError(`Failed to get EC2 status: ${response.status} ${text}`);
|
|
147
|
+
}
|
|
148
|
+
return response.json();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Wait for SSH to be available on the remote host
|
|
152
|
+
*/
|
|
153
|
+
async function waitForSSH(host, port, timeoutMs = 60000) {
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
156
|
+
try {
|
|
157
|
+
await new Promise((resolve, reject) => {
|
|
158
|
+
const socket = net.createConnection({ host, port, timeout: 5000 });
|
|
159
|
+
socket.on("connect", () => {
|
|
160
|
+
socket.destroy();
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
socket.on("error", reject);
|
|
164
|
+
socket.on("timeout", () => {
|
|
165
|
+
socket.destroy();
|
|
166
|
+
reject(new Error("Timeout"));
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
return; // SSH is available
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Wait and retry
|
|
173
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw new SSHTunnelError(`SSH not available after ${timeoutMs / 1000} seconds`);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Establish reverse SSH tunnel for SSHFS
|
|
180
|
+
*
|
|
181
|
+
* This creates a tunnel where:
|
|
182
|
+
* - Local: User's machine listens on a port
|
|
183
|
+
* - Remote: EC2 can connect back through the tunnel
|
|
184
|
+
*/
|
|
185
|
+
async function establishReverseTunnel(connectionInfo, localPath, sshKeyPath, onProgress) {
|
|
186
|
+
const localPort = await findAvailablePort();
|
|
187
|
+
const remotePort = await findAvailablePort(); // Port on EC2 for the reverse tunnel
|
|
188
|
+
onProgress?.(`Establishing SSH tunnel to ${connectionInfo.public_ip}...`);
|
|
189
|
+
// Start a local SSH server that allows the remote to connect back
|
|
190
|
+
// We use SSH reverse port forwarding: -R remotePort:localhost:22
|
|
191
|
+
// But actually, for SSHFS we need the remote to access our files
|
|
192
|
+
//
|
|
193
|
+
// Better approach: Use SSH with -R to create a reverse tunnel
|
|
194
|
+
// Then remote does: sshfs -p <tunnelPort> user@localhost:<path> /mnt/user-code
|
|
195
|
+
const sshArgs = [
|
|
196
|
+
"-o", "StrictHostKeyChecking=no",
|
|
197
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
198
|
+
"-o", "ServerAliveInterval=30",
|
|
199
|
+
"-o", "ServerAliveCountMax=3",
|
|
200
|
+
"-i", sshKeyPath,
|
|
201
|
+
// Reverse tunnel: remote port -> local SSH (for SSHFS)
|
|
202
|
+
"-R", `${remotePort}:localhost:22`,
|
|
203
|
+
"-p", String(connectionInfo.ssh_port),
|
|
204
|
+
`${connectionInfo.ssh_user}@${connectionInfo.public_ip}`,
|
|
205
|
+
// Keep connection alive, don't execute command
|
|
206
|
+
"-N",
|
|
207
|
+
];
|
|
208
|
+
const tunnelProc = (0, node_child_process_1.spawn)("ssh", sshArgs, {
|
|
209
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
210
|
+
detached: false,
|
|
211
|
+
});
|
|
212
|
+
let stderr = "";
|
|
213
|
+
tunnelProc.stderr?.on("data", (data) => {
|
|
214
|
+
stderr += data.toString();
|
|
215
|
+
});
|
|
216
|
+
// Wait for tunnel to establish
|
|
217
|
+
await new Promise((resolve, reject) => {
|
|
218
|
+
const timeout = setTimeout(() => {
|
|
219
|
+
tunnelProc.kill();
|
|
220
|
+
reject(new SSHTunnelError(`SSH tunnel timeout: ${stderr}`));
|
|
221
|
+
}, 30000);
|
|
222
|
+
tunnelProc.on("error", (err) => {
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
reject(new SSHTunnelError(`SSH tunnel failed: ${err.message}`));
|
|
225
|
+
});
|
|
226
|
+
// Give it a moment to establish
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
if (tunnelProc.killed || tunnelProc.exitCode !== null) {
|
|
229
|
+
reject(new SSHTunnelError(`SSH tunnel exited early: ${stderr}`));
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
clearTimeout(timeout);
|
|
233
|
+
resolve();
|
|
234
|
+
}
|
|
235
|
+
}, 3000);
|
|
236
|
+
});
|
|
237
|
+
onProgress?.("SSH tunnel established");
|
|
238
|
+
return {
|
|
239
|
+
process: tunnelProc,
|
|
240
|
+
localPort,
|
|
241
|
+
remoteHost: connectionInfo.public_ip,
|
|
242
|
+
remoteUser: connectionInfo.ssh_user,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Run analysis on the remote EC2 via SSH
|
|
247
|
+
*
|
|
248
|
+
* Instead of SSHFS (complex), we'll:
|
|
249
|
+
* 1. Tar the codebase locally
|
|
250
|
+
* 2. Stream it to the remote via SSH
|
|
251
|
+
* 3. Run Claude there
|
|
252
|
+
* 4. Get results back
|
|
253
|
+
*/
|
|
254
|
+
async function runRemoteAnalysis(connectionInfo, localPath, sshKeyPath, analysisPrompt, timeout, onProgress) {
|
|
255
|
+
const absolutePath = path.resolve(localPath);
|
|
256
|
+
onProgress?.("Preparing codebase for remote analysis...");
|
|
257
|
+
// Create a tar of the codebase (excluding node_modules, .git, etc.)
|
|
258
|
+
const excludes = [
|
|
259
|
+
"--exclude=node_modules",
|
|
260
|
+
"--exclude=.git",
|
|
261
|
+
"--exclude=dist",
|
|
262
|
+
"--exclude=build",
|
|
263
|
+
"--exclude=.next",
|
|
264
|
+
"--exclude=coverage",
|
|
265
|
+
"--exclude=*.log",
|
|
266
|
+
];
|
|
267
|
+
// Remote command:
|
|
268
|
+
// 1. Create temp directory
|
|
269
|
+
// 2. Extract tar
|
|
270
|
+
// 3. Run claude --print -p <prompt>
|
|
271
|
+
// 4. Clean up
|
|
272
|
+
const remoteScript = `
|
|
273
|
+
set -e
|
|
274
|
+
WORK_DIR=$(mktemp -d)
|
|
275
|
+
cd "$WORK_DIR"
|
|
276
|
+
tar xzf -
|
|
277
|
+
source /etc/profile.d/anthropic.sh 2>/dev/null || true
|
|
278
|
+
claude --print -p '${analysisPrompt.replace(/'/g, "'\\''")}'
|
|
279
|
+
rm -rf "$WORK_DIR"
|
|
280
|
+
`;
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
// Pipe: tar -> ssh -> remote extraction + claude
|
|
283
|
+
const tarProc = (0, node_child_process_1.spawn)("tar", [
|
|
284
|
+
"czf", "-",
|
|
285
|
+
...excludes,
|
|
286
|
+
"-C", path.dirname(absolutePath),
|
|
287
|
+
path.basename(absolutePath),
|
|
288
|
+
], {
|
|
289
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
290
|
+
});
|
|
291
|
+
const sshProc = (0, node_child_process_1.spawn)("ssh", [
|
|
292
|
+
"-o", "StrictHostKeyChecking=no",
|
|
293
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
294
|
+
"-i", sshKeyPath,
|
|
295
|
+
"-p", String(connectionInfo.ssh_port),
|
|
296
|
+
`${connectionInfo.ssh_user}@${connectionInfo.public_ip}`,
|
|
297
|
+
"bash", "-c", remoteScript,
|
|
298
|
+
], {
|
|
299
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
300
|
+
});
|
|
301
|
+
// Pipe tar output to ssh stdin
|
|
302
|
+
tarProc.stdout.pipe(sshProc.stdin);
|
|
303
|
+
let stdout = "";
|
|
304
|
+
let stderr = "";
|
|
305
|
+
sshProc.stdout.on("data", (data) => {
|
|
306
|
+
stdout += data.toString();
|
|
307
|
+
});
|
|
308
|
+
sshProc.stderr.on("data", (data) => {
|
|
309
|
+
stderr += data.toString();
|
|
310
|
+
});
|
|
311
|
+
tarProc.on("error", (err) => {
|
|
312
|
+
reject(new RemoteAnalyzerError(`Tar failed: ${err.message}`));
|
|
313
|
+
});
|
|
314
|
+
const timeoutId = setTimeout(() => {
|
|
315
|
+
tarProc.kill();
|
|
316
|
+
sshProc.kill();
|
|
317
|
+
reject(new RemoteAnalyzerError(`Remote analysis timed out after ${timeout} seconds`));
|
|
318
|
+
}, timeout * 1000);
|
|
319
|
+
sshProc.on("close", (code) => {
|
|
320
|
+
clearTimeout(timeoutId);
|
|
321
|
+
if (code !== 0) {
|
|
322
|
+
reject(new RemoteAnalyzerError(`Remote analysis failed (code ${code}): ${stderr}`));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
resolve(stdout);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
sshProc.on("error", (err) => {
|
|
329
|
+
clearTimeout(timeoutId);
|
|
330
|
+
reject(new RemoteAnalyzerError(`SSH failed: ${err.message}`));
|
|
331
|
+
});
|
|
332
|
+
onProgress?.("Uploading codebase and running analysis...");
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Clean up SSH key files
|
|
337
|
+
*/
|
|
338
|
+
function cleanupSSHKey(privateKeyPath) {
|
|
339
|
+
try {
|
|
340
|
+
fs.unlinkSync(privateKeyPath);
|
|
341
|
+
fs.unlinkSync(`${privateKeyPath}.pub`);
|
|
342
|
+
fs.rmdirSync(path.dirname(privateKeyPath));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Ignore cleanup errors
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Main entry point: Analyze codebase using remote Claude Code
|
|
350
|
+
*/
|
|
351
|
+
async function analyzeWithRemoteClaude(codebasePath, options = {}, onProgress) {
|
|
352
|
+
const { timeout = 300, apiEndpoint = DEFAULT_API_ENDPOINT, } = options;
|
|
353
|
+
if (!apiEndpoint) {
|
|
354
|
+
throw new RemoteAnalyzerError("API endpoint not available. Please check your network connection or try again later.");
|
|
355
|
+
}
|
|
356
|
+
// Validate local path
|
|
357
|
+
(0, analyzer_js_1.validateCodebasePath)(codebasePath);
|
|
358
|
+
const absolutePath = path.resolve(codebasePath);
|
|
359
|
+
let sshKeyPath = null;
|
|
360
|
+
try {
|
|
361
|
+
// 1. Start EC2 instance
|
|
362
|
+
onProgress?.("Starting remote analysis server...");
|
|
363
|
+
const connectionInfo = await startEC2Instance(apiEndpoint);
|
|
364
|
+
onProgress?.(`Server ready at ${connectionInfo.public_ip}`);
|
|
365
|
+
// 2. Wait for SSH
|
|
366
|
+
onProgress?.("Waiting for SSH to be available...");
|
|
367
|
+
await waitForSSH(connectionInfo.public_ip, connectionInfo.ssh_port);
|
|
368
|
+
// 3. Generate temp SSH key (for secure connection)
|
|
369
|
+
// Note: In production, you'd use a pre-shared key or other auth
|
|
370
|
+
// For now, we assume the EC2 has a known authorized key
|
|
371
|
+
// const { privateKeyPath, publicKey } = await generateTempSSHKey();
|
|
372
|
+
// sshKeyPath = privateKeyPath;
|
|
373
|
+
// For now, use the user's default SSH key or a configured one
|
|
374
|
+
sshKeyPath = options.sshKeyPath || path.join(os.homedir(), ".ssh", "id_rsa");
|
|
375
|
+
if (!fs.existsSync(sshKeyPath)) {
|
|
376
|
+
// Try ed25519
|
|
377
|
+
sshKeyPath = path.join(os.homedir(), ".ssh", "id_ed25519");
|
|
378
|
+
if (!fs.existsSync(sshKeyPath)) {
|
|
379
|
+
throw new SSHTunnelError("No SSH key found. Please create one with 'ssh-keygen' or specify --ssh-key");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// 4. Run remote analysis
|
|
383
|
+
const analysisPrompt = `Analyze this frontend codebase and extract:
|
|
384
|
+
|
|
385
|
+
1. All page routes and their component files
|
|
386
|
+
2. All API calls (fetch, axios, etc.) with endpoints and HTTP methods
|
|
387
|
+
3. Form action types
|
|
388
|
+
4. Map API endpoints to their likely tool names
|
|
389
|
+
|
|
390
|
+
Return a JSON object with this EXACT structure (no markdown, no explanation, just JSON):
|
|
391
|
+
{
|
|
392
|
+
"routes": [
|
|
393
|
+
{"route_name": "string", "file_path": "string", "component_name": "string"}
|
|
394
|
+
],
|
|
395
|
+
"api_calls": [
|
|
396
|
+
{"endpoint": "string", "method": "string", "component_file": "string", "action_form_type": "string or null"}
|
|
397
|
+
],
|
|
398
|
+
"mappings": [
|
|
399
|
+
{
|
|
400
|
+
"tool_name": "string",
|
|
401
|
+
"api_endpoint": "string",
|
|
402
|
+
"navigation_page": "string or null",
|
|
403
|
+
"action_form_type": "string or null",
|
|
404
|
+
"frontend_component": "string or null",
|
|
405
|
+
"frontend_file_path": "string or null"
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
}`;
|
|
409
|
+
onProgress?.("Running Claude Code analysis...");
|
|
410
|
+
const rawResult = await runRemoteAnalysis(connectionInfo, absolutePath, sshKeyPath, analysisPrompt, timeout, onProgress);
|
|
411
|
+
// 5. Parse results
|
|
412
|
+
onProgress?.("Parsing results...");
|
|
413
|
+
const data = (0, analyzer_js_1.parseClaudeResponse)(rawResult);
|
|
414
|
+
return (0, analyzer_js_1.buildAnalysisResult)(data, absolutePath);
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
// Cleanup would go here if we generated temp keys
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Check if remote analysis is available (API endpoint configured)
|
|
422
|
+
*/
|
|
423
|
+
function isRemoteAnalysisAvailable() {
|
|
424
|
+
return !!DEFAULT_API_ENDPOINT;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Get the configured API endpoint
|
|
428
|
+
*/
|
|
429
|
+
function getApiEndpoint() {
|
|
430
|
+
return DEFAULT_API_ENDPOINT;
|
|
431
|
+
}
|
package/dist/types.d.ts
CHANGED