@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 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
- // Check Claude CLI is available
213
+ const apiEndpoint = options.apiEndpoint || process.env.KITE_API_ENDPOINT;
106
214
  const spinner = (0, ora_1.default)();
107
- if (!quiet) {
108
- spinner.start("Checking Claude CLI...");
109
- }
110
- const claudeAvailable = await (0, analyzer_js_1.checkClaudeAvailable)();
111
- if (!claudeAvailable) {
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
- if (!quiet)
119
- spinner.succeed("Claude CLI found");
120
- // Run analysis
222
+ // Remote analysis mode (only mode available)
121
223
  if (!quiet) {
122
- spinner.start(`Analyzing codebase: ${codebasePath}`);
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, analyzer_js_1.analyzeWithClaude)(codebasePath, {
233
+ const result = await (0, remote_analyzer_js_1.analyzeWithRemoteClaude)(codebasePath, {
130
234
  timeout: parseInt(options.timeout, 10),
131
235
  verbose,
132
- });
133
- if (!quiet) {
134
- spinner.succeed("Analysis complete");
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
- 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
- }
240
+ spinner.text = message;
232
241
  }
242
+ });
243
+ if (!quiet) {
244
+ spinner.succeed("Remote analysis complete");
233
245
  }
234
- console.log();
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 analyzer_js_1.ClaudeNotFoundError) {
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 analyzer_js_1.ClaudeParseError) {
248
- console.log(chalk_1.default.red(`\n✗ Failed to parse Claude response\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 analyzer_js_1.AnalyzerError) {
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
@@ -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.2",
4
4
  "description": "Kite CLI - Analyze frontend codebases and map API calls",
5
5
  "main": "dist/index.js",
6
6
  "bin": {