@kite-copilot/cli 1.0.0 → 1.0.1
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 +184 -101
- 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,12 +204,85 @@ 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("--local", "Use local Claude CLI instead of remote server")
|
|
208
|
+
.option("--remote", "Force use of remote analysis server")
|
|
209
|
+
.option("--api-endpoint <url>", "Remote analysis API endpoint (or set KITE_API_ENDPOINT)")
|
|
210
|
+
.option("--ssh-key <path>", "SSH private key for remote server authentication")
|
|
101
211
|
.action(async (codebasePath, options) => {
|
|
102
212
|
const quiet = options.quiet || false;
|
|
103
213
|
const verbose = options.verbose || false;
|
|
104
214
|
const skipPrompt = options.prompt === false;
|
|
105
|
-
|
|
215
|
+
const forceLocal = options.local || false;
|
|
216
|
+
const forceRemote = options.remote || false;
|
|
217
|
+
const apiEndpoint = options.apiEndpoint || process.env.KITE_API_ENDPOINT;
|
|
106
218
|
const spinner = (0, ora_1.default)();
|
|
219
|
+
// Determine whether to use remote or local analysis
|
|
220
|
+
const remoteAvailable = (0, remote_analyzer_js_1.isRemoteAnalysisAvailable)() || !!apiEndpoint;
|
|
221
|
+
let useRemote = remoteAvailable && !forceLocal;
|
|
222
|
+
if (forceRemote && !remoteAvailable && !apiEndpoint) {
|
|
223
|
+
console.log(chalk_1.default.red("\n✗ Remote analysis requested but not configured.\n"));
|
|
224
|
+
console.log(chalk_1.default.gray(" Set KITE_API_ENDPOINT or deploy the infrastructure first.\n"));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
if (forceRemote) {
|
|
228
|
+
useRemote = true;
|
|
229
|
+
}
|
|
230
|
+
if (useRemote) {
|
|
231
|
+
// Remote analysis mode
|
|
232
|
+
if (!quiet) {
|
|
233
|
+
spinner.start("Preparing remote analysis...");
|
|
234
|
+
}
|
|
235
|
+
if (verbose) {
|
|
236
|
+
console.log(chalk_1.default.gray(`[DEBUG] Using remote analysis`));
|
|
237
|
+
console.log(chalk_1.default.gray(`[DEBUG] API endpoint: ${apiEndpoint || "(from env)"}`));
|
|
238
|
+
console.log(chalk_1.default.gray(`[DEBUG] Path: ${codebasePath}`));
|
|
239
|
+
console.log(chalk_1.default.gray(`[DEBUG] Timeout: ${options.timeout}s`));
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const result = await (0, remote_analyzer_js_1.analyzeWithRemoteClaude)(codebasePath, {
|
|
243
|
+
timeout: parseInt(options.timeout, 10),
|
|
244
|
+
verbose,
|
|
245
|
+
apiEndpoint,
|
|
246
|
+
sshKeyPath: options.sshKey,
|
|
247
|
+
}, (message) => {
|
|
248
|
+
if (!quiet) {
|
|
249
|
+
spinner.text = message;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
if (!quiet) {
|
|
253
|
+
spinner.succeed("Remote analysis complete");
|
|
254
|
+
}
|
|
255
|
+
// Continue with result handling (shared with local)
|
|
256
|
+
await handleAnalysisResult(result, options, quiet, verbose, spinner, skipPrompt);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (!quiet)
|
|
261
|
+
spinner.fail("Remote analysis failed");
|
|
262
|
+
if (error instanceof remote_analyzer_js_1.EC2StartError) {
|
|
263
|
+
console.log(chalk_1.default.red(`\n✗ Failed to start remote server: ${error.message}\n`));
|
|
264
|
+
}
|
|
265
|
+
else if (error instanceof remote_analyzer_js_1.SSHTunnelError) {
|
|
266
|
+
console.log(chalk_1.default.red(`\n✗ SSH connection failed: ${error.message}\n`));
|
|
267
|
+
}
|
|
268
|
+
else if (error instanceof remote_analyzer_js_1.RemoteAnalyzerError) {
|
|
269
|
+
console.log(chalk_1.default.red(`\n✗ ${error.message}\n`));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.log(chalk_1.default.red(`\n✗ ${error.message}\n`));
|
|
273
|
+
}
|
|
274
|
+
// Offer to fall back to local if not forced remote
|
|
275
|
+
if (!forceRemote) {
|
|
276
|
+
console.log(chalk_1.default.yellow("Falling back to local analysis...\n"));
|
|
277
|
+
useRemote = false;
|
|
278
|
+
// Continue to local analysis below
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Local analysis mode (or fallback)
|
|
107
286
|
if (!quiet) {
|
|
108
287
|
spinner.start("Checking Claude CLI...");
|
|
109
288
|
}
|
|
@@ -111,8 +290,9 @@ program
|
|
|
111
290
|
if (!claudeAvailable) {
|
|
112
291
|
if (!quiet)
|
|
113
292
|
spinner.fail("Claude CLI not found");
|
|
114
|
-
console.log(chalk_1.default.red("\n✗ Claude CLI is required for analysis.\n"));
|
|
293
|
+
console.log(chalk_1.default.red("\n✗ Claude CLI is required for local analysis.\n"));
|
|
115
294
|
console.log(chalk_1.default.gray(" Install it with: npm install -g @anthropic-ai/claude-cli\n"));
|
|
295
|
+
console.log(chalk_1.default.gray(" Or configure remote analysis with KITE_API_ENDPOINT\n"));
|
|
116
296
|
process.exit(1);
|
|
117
297
|
}
|
|
118
298
|
if (!quiet)
|
|
@@ -122,6 +302,7 @@ program
|
|
|
122
302
|
spinner.start(`Analyzing codebase: ${codebasePath}`);
|
|
123
303
|
}
|
|
124
304
|
if (verbose) {
|
|
305
|
+
console.log(chalk_1.default.gray(`[DEBUG] Using local analysis`));
|
|
125
306
|
console.log(chalk_1.default.gray(`[DEBUG] Path: ${codebasePath}`));
|
|
126
307
|
console.log(chalk_1.default.gray(`[DEBUG] Timeout: ${options.timeout}s`));
|
|
127
308
|
}
|
|
@@ -133,105 +314,7 @@ program
|
|
|
133
314
|
if (!quiet) {
|
|
134
315
|
spinner.succeed("Analysis complete");
|
|
135
316
|
}
|
|
136
|
-
|
|
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);
|
|
183
|
-
if (!quiet) {
|
|
184
|
-
console.log(chalk_1.default.green(`✓ Saved mappings to: ${outputPath}`));
|
|
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
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
console.log();
|
|
317
|
+
await handleAnalysisResult(result, options, quiet, verbose, spinner, skipPrompt);
|
|
235
318
|
}
|
|
236
319
|
catch (error) {
|
|
237
320
|
if (!quiet)
|
|
@@ -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
|
+
// Default API Gateway endpoint (should be configured via env or config)
|
|
53
|
+
const DEFAULT_API_ENDPOINT = process.env.KITE_API_ENDPOINT || "";
|
|
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 configured. Set KITE_API_ENDPOINT environment variable or deploy infrastructure first.");
|
|
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