@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 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
- // Check Claude CLI is available
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
- 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);
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
@@ -33,6 +33,8 @@ export interface AnalyzeOptions {
33
33
  timeout?: number;
34
34
  verbose?: boolean;
35
35
  claudePath?: string;
36
+ sshKeyPath?: string;
37
+ apiEndpoint?: string;
36
38
  }
37
39
  export interface UploadResult {
38
40
  success: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kite-copilot/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Kite CLI - Analyze frontend codebases and map API calls",
5
5
  "main": "dist/index.js",
6
6
  "bin": {