mcp-inflight 0.2.0
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/README.md +107 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1711 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1711 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ListToolsRequestSchema
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
|
|
13
|
+
// src/tools/deploy.ts
|
|
14
|
+
import * as path4 from "path";
|
|
15
|
+
import * as fs4 from "fs";
|
|
16
|
+
|
|
17
|
+
// src/utils/detect-project.ts
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
function hasDependency(pkg, name) {
|
|
21
|
+
return !!(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
22
|
+
}
|
|
23
|
+
function detectPackageManager(projectPath, pkg) {
|
|
24
|
+
if (pkg.packageManager) {
|
|
25
|
+
if (pkg.packageManager.startsWith("pnpm")) return "pnpm";
|
|
26
|
+
if (pkg.packageManager.startsWith("yarn")) return "yarn";
|
|
27
|
+
if (pkg.packageManager.startsWith("bun")) return "bun";
|
|
28
|
+
if (pkg.packageManager.startsWith("npm")) return "npm";
|
|
29
|
+
}
|
|
30
|
+
if (fs.existsSync(path.join(projectPath, "pnpm-lock.yaml"))) return "pnpm";
|
|
31
|
+
if (fs.existsSync(path.join(projectPath, "yarn.lock"))) return "yarn";
|
|
32
|
+
if (fs.existsSync(path.join(projectPath, "bun.lockb"))) return "bun";
|
|
33
|
+
if (fs.existsSync(path.join(projectPath, "package-lock.json"))) return "npm";
|
|
34
|
+
return "npm";
|
|
35
|
+
}
|
|
36
|
+
function isMonorepo(projectPath, pkg) {
|
|
37
|
+
if (pkg.workspaces) return true;
|
|
38
|
+
const monorepoConfigs = [
|
|
39
|
+
"turbo.json",
|
|
40
|
+
"lerna.json",
|
|
41
|
+
"nx.json",
|
|
42
|
+
"pnpm-workspace.yaml",
|
|
43
|
+
"rush.json"
|
|
44
|
+
];
|
|
45
|
+
return monorepoConfigs.some(
|
|
46
|
+
(config) => fs.existsSync(path.join(projectPath, config))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function extractPortFromScript(script) {
|
|
50
|
+
const portPatterns = [
|
|
51
|
+
/--port[=\s]+(\d+)/,
|
|
52
|
+
/-p[=\s]+(\d+)/,
|
|
53
|
+
/PORT[=:]\s*(\d+)/,
|
|
54
|
+
/:(\d{4,5})/
|
|
55
|
+
// Match port in URLs like localhost:3000
|
|
56
|
+
];
|
|
57
|
+
for (const pattern of portPatterns) {
|
|
58
|
+
const match = script.match(pattern);
|
|
59
|
+
if (match) {
|
|
60
|
+
return parseInt(match[1], 10);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function findBestDevScript(scripts, packageManager) {
|
|
66
|
+
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
67
|
+
const devScriptPriority = [
|
|
68
|
+
// Monorepo workspace scripts (common patterns)
|
|
69
|
+
"web:dev",
|
|
70
|
+
"app:dev",
|
|
71
|
+
"frontend:dev",
|
|
72
|
+
"client:dev",
|
|
73
|
+
"dev:web",
|
|
74
|
+
"dev:app",
|
|
75
|
+
// Standard dev scripts
|
|
76
|
+
"dev",
|
|
77
|
+
"start:dev",
|
|
78
|
+
"serve",
|
|
79
|
+
// Start scripts
|
|
80
|
+
"start"
|
|
81
|
+
];
|
|
82
|
+
for (const scriptName of devScriptPriority) {
|
|
83
|
+
if (scripts[scriptName]) {
|
|
84
|
+
const scriptContent = scripts[scriptName];
|
|
85
|
+
const port = extractPortFromScript(scriptContent);
|
|
86
|
+
return {
|
|
87
|
+
scriptName,
|
|
88
|
+
command: `${runCmd} ${scriptName}`,
|
|
89
|
+
port: port ?? inferPortFromScript(scriptContent)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const devScript = Object.keys(scripts).find(
|
|
94
|
+
(name) => name.includes("dev") && !name.includes("devDependencies")
|
|
95
|
+
);
|
|
96
|
+
if (devScript) {
|
|
97
|
+
const scriptContent = scripts[devScript];
|
|
98
|
+
return {
|
|
99
|
+
scriptName: devScript,
|
|
100
|
+
command: `${runCmd} ${devScript}`,
|
|
101
|
+
port: extractPortFromScript(scriptContent) ?? inferPortFromScript(scriptContent)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function inferPortFromScript(script) {
|
|
107
|
+
if (script.includes("next")) return 3e3;
|
|
108
|
+
if (script.includes("vite")) return 5173;
|
|
109
|
+
if (script.includes("webpack-dev-server")) return 8080;
|
|
110
|
+
if (script.includes("react-scripts")) return 3e3;
|
|
111
|
+
if (script.includes("serve")) return 3e3;
|
|
112
|
+
if (script.includes("turbo")) return 3e3;
|
|
113
|
+
return 3e3;
|
|
114
|
+
}
|
|
115
|
+
function analyzeCompatibility(projectPath, pkg, isRepo) {
|
|
116
|
+
const issues = [];
|
|
117
|
+
const warnings = [];
|
|
118
|
+
const incompatibleDeps = [
|
|
119
|
+
{ name: "electron", reason: "Electron apps require native desktop environment" },
|
|
120
|
+
{ name: "tauri", reason: "Tauri apps require native desktop environment" },
|
|
121
|
+
{ name: "@napi-rs", reason: "Native Node.js addons are not supported" }
|
|
122
|
+
];
|
|
123
|
+
for (const dep of incompatibleDeps) {
|
|
124
|
+
if (hasDependency(pkg, dep.name)) {
|
|
125
|
+
issues.push(`${dep.name}: ${dep.reason}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const problematicDeps = [
|
|
129
|
+
{ name: "sharp", reason: "May require native binaries - ensure libvips is available" },
|
|
130
|
+
{ name: "canvas", reason: "Requires Cairo graphics library" },
|
|
131
|
+
{ name: "sqlite3", reason: "Native SQLite bindings may need recompilation" },
|
|
132
|
+
{ name: "bcrypt", reason: "Native bindings - consider using bcryptjs instead" },
|
|
133
|
+
{ name: "puppeteer", reason: "Requires Chromium - use puppeteer-core with external browser" }
|
|
134
|
+
];
|
|
135
|
+
for (const dep of problematicDeps) {
|
|
136
|
+
if (hasDependency(pkg, dep.name)) {
|
|
137
|
+
warnings.push(`${dep.name}: ${dep.reason}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (fs.existsSync(path.join(projectPath, "Dockerfile")) || fs.existsSync(path.join(projectPath, "docker-compose.yml"))) {
|
|
141
|
+
warnings.push("Project uses Docker - CodeSandbox runs containers natively, Docker-in-Docker not supported");
|
|
142
|
+
}
|
|
143
|
+
if (isRepo) {
|
|
144
|
+
warnings.push("Monorepo detected - consider deploying only the specific app/package needed");
|
|
145
|
+
}
|
|
146
|
+
const serviceIndicators = [
|
|
147
|
+
{ file: "redis.conf", service: "Redis" },
|
|
148
|
+
{ file: "elasticsearch.yml", service: "Elasticsearch" },
|
|
149
|
+
{ file: "mongod.conf", service: "MongoDB" }
|
|
150
|
+
];
|
|
151
|
+
for (const indicator of serviceIndicators) {
|
|
152
|
+
if (fs.existsSync(path.join(projectPath, indicator.file))) {
|
|
153
|
+
warnings.push(`${indicator.service} config found - ensure service is available or use cloud-hosted version`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
canDeploy: issues.length === 0,
|
|
158
|
+
issues,
|
|
159
|
+
warnings
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function analyzeSizing(projectPath) {
|
|
163
|
+
const reasons = [];
|
|
164
|
+
const largeFiles = [];
|
|
165
|
+
let totalSize = 0;
|
|
166
|
+
let fileCount = 0;
|
|
167
|
+
const skipDirs = /* @__PURE__ */ new Set([
|
|
168
|
+
"node_modules",
|
|
169
|
+
".git",
|
|
170
|
+
".next",
|
|
171
|
+
"dist",
|
|
172
|
+
"build",
|
|
173
|
+
".cache",
|
|
174
|
+
".turbo",
|
|
175
|
+
".vercel",
|
|
176
|
+
"coverage",
|
|
177
|
+
".nyc_output"
|
|
178
|
+
]);
|
|
179
|
+
const LARGE_FILE_THRESHOLD = 1024 * 1024;
|
|
180
|
+
function walkDir(dir, relativePath = "") {
|
|
181
|
+
try {
|
|
182
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (skipDirs.has(entry.name)) continue;
|
|
185
|
+
const fullPath = path.join(dir, entry.name);
|
|
186
|
+
const relPath = path.join(relativePath, entry.name);
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
walkDir(fullPath, relPath);
|
|
189
|
+
} else if (entry.isFile()) {
|
|
190
|
+
try {
|
|
191
|
+
const stats = fs.statSync(fullPath);
|
|
192
|
+
totalSize += stats.size;
|
|
193
|
+
fileCount++;
|
|
194
|
+
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
195
|
+
largeFiles.push(`${relPath} (${(stats.size / 1024 / 1024).toFixed(1)}MB)`);
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
walkDir(projectPath);
|
|
205
|
+
const estimatedSizeMB = totalSize / 1024 / 1024;
|
|
206
|
+
let recommendedTier = "Micro";
|
|
207
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
208
|
+
let depCount = 0;
|
|
209
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
210
|
+
try {
|
|
211
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
212
|
+
depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (estimatedSizeMB < 5 && fileCount < 50 && depCount < 20) {
|
|
217
|
+
recommendedTier = "Nano";
|
|
218
|
+
reasons.push("Small project with few files and dependencies");
|
|
219
|
+
} else if (estimatedSizeMB < 50 && fileCount < 500 && depCount < 50) {
|
|
220
|
+
recommendedTier = "Micro";
|
|
221
|
+
reasons.push("Standard project size");
|
|
222
|
+
} else if (estimatedSizeMB < 200 && fileCount < 2e3 && depCount < 100) {
|
|
223
|
+
recommendedTier = "Small";
|
|
224
|
+
reasons.push("Medium-sized project");
|
|
225
|
+
} else if (estimatedSizeMB < 500 && fileCount < 5e3) {
|
|
226
|
+
recommendedTier = "Medium";
|
|
227
|
+
reasons.push("Large project with many files");
|
|
228
|
+
} else {
|
|
229
|
+
recommendedTier = "Large";
|
|
230
|
+
reasons.push("Very large project");
|
|
231
|
+
}
|
|
232
|
+
if (fs.existsSync(path.join(projectPath, "turbo.json"))) {
|
|
233
|
+
if (recommendedTier === "Nano") recommendedTier = "Micro";
|
|
234
|
+
if (recommendedTier === "Micro") recommendedTier = "Small";
|
|
235
|
+
reasons.push("Turborepo monorepo - builds may need more resources");
|
|
236
|
+
}
|
|
237
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
238
|
+
try {
|
|
239
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
240
|
+
if (hasDependency(pkg, "next") && (hasDependency(pkg, "prisma") || hasDependency(pkg, "@prisma/client"))) {
|
|
241
|
+
if (recommendedTier === "Nano") recommendedTier = "Micro";
|
|
242
|
+
if (recommendedTier === "Micro") recommendedTier = "Small";
|
|
243
|
+
reasons.push("Next.js + Prisma requires more memory for generation");
|
|
244
|
+
}
|
|
245
|
+
if (hasDependency(pkg, "typescript") && depCount > 80) {
|
|
246
|
+
reasons.push("Large TypeScript project - compilation may be slow on smaller tiers");
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (largeFiles.length > 0) {
|
|
252
|
+
reasons.push(`${largeFiles.length} large file(s) detected`);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
recommendedTier,
|
|
256
|
+
fileCount,
|
|
257
|
+
estimatedSizeMB: Math.round(estimatedSizeMB * 10) / 10,
|
|
258
|
+
largeFiles: largeFiles.slice(0, 5),
|
|
259
|
+
// Limit to top 5
|
|
260
|
+
reasons
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function getSuggestedScripts(scripts) {
|
|
264
|
+
const devRelated = [
|
|
265
|
+
"dev",
|
|
266
|
+
"start",
|
|
267
|
+
"serve",
|
|
268
|
+
"web:dev",
|
|
269
|
+
"app:dev",
|
|
270
|
+
"build",
|
|
271
|
+
"test"
|
|
272
|
+
];
|
|
273
|
+
const suggestions = [];
|
|
274
|
+
for (const name of devRelated) {
|
|
275
|
+
if (scripts[name]) {
|
|
276
|
+
suggestions.push(name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const name of Object.keys(scripts)) {
|
|
280
|
+
if ((name.includes("dev") || name.includes("start")) && !suggestions.includes(name)) {
|
|
281
|
+
suggestions.push(name);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return suggestions.slice(0, 10);
|
|
285
|
+
}
|
|
286
|
+
function detectProjectType(projectPath) {
|
|
287
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
288
|
+
const indexHtmlPath = path.join(projectPath, "index.html");
|
|
289
|
+
const sizing = analyzeSizing(projectPath);
|
|
290
|
+
const defaultCompatibility = {
|
|
291
|
+
canDeploy: true,
|
|
292
|
+
issues: [],
|
|
293
|
+
warnings: []
|
|
294
|
+
};
|
|
295
|
+
const defaultResult = {
|
|
296
|
+
type: "static",
|
|
297
|
+
port: 3e3,
|
|
298
|
+
installCommand: null,
|
|
299
|
+
startCommand: "npx serve .",
|
|
300
|
+
detectedPackageManager: "npm",
|
|
301
|
+
isMonorepo: false,
|
|
302
|
+
availableScripts: [],
|
|
303
|
+
suggestedScripts: [],
|
|
304
|
+
compatibility: defaultCompatibility,
|
|
305
|
+
sizing
|
|
306
|
+
};
|
|
307
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
308
|
+
if (fs.existsSync(indexHtmlPath)) {
|
|
309
|
+
return defaultResult;
|
|
310
|
+
}
|
|
311
|
+
return defaultResult;
|
|
312
|
+
}
|
|
313
|
+
const packageJson = JSON.parse(
|
|
314
|
+
fs.readFileSync(packageJsonPath, "utf-8")
|
|
315
|
+
);
|
|
316
|
+
const packageManager = detectPackageManager(projectPath, packageJson);
|
|
317
|
+
const isRepo = isMonorepo(projectPath, packageJson);
|
|
318
|
+
const scripts = packageJson.scripts ?? {};
|
|
319
|
+
const availableScripts = Object.keys(scripts);
|
|
320
|
+
const suggestedScripts = getSuggestedScripts(scripts);
|
|
321
|
+
const compatibility = analyzeCompatibility(projectPath, packageJson, isRepo);
|
|
322
|
+
const installCommand = `${packageManager} install`;
|
|
323
|
+
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
324
|
+
const bestScript = findBestDevScript(scripts, packageManager);
|
|
325
|
+
let type = "node";
|
|
326
|
+
let defaultPort = 3e3;
|
|
327
|
+
if (isRepo) {
|
|
328
|
+
type = "monorepo";
|
|
329
|
+
} else if (hasDependency(packageJson, "next")) {
|
|
330
|
+
type = "nextjs";
|
|
331
|
+
defaultPort = 3e3;
|
|
332
|
+
} else if (hasDependency(packageJson, "vite")) {
|
|
333
|
+
type = "vite";
|
|
334
|
+
defaultPort = 5173;
|
|
335
|
+
} else if (hasDependency(packageJson, "react-scripts")) {
|
|
336
|
+
type = "cra";
|
|
337
|
+
defaultPort = 3e3;
|
|
338
|
+
} else if (hasDependency(packageJson, "express") || hasDependency(packageJson, "fastify") || hasDependency(packageJson, "koa") || hasDependency(packageJson, "hono")) {
|
|
339
|
+
type = "node";
|
|
340
|
+
defaultPort = 3e3;
|
|
341
|
+
}
|
|
342
|
+
if (bestScript) {
|
|
343
|
+
return {
|
|
344
|
+
type,
|
|
345
|
+
port: bestScript.port,
|
|
346
|
+
installCommand,
|
|
347
|
+
startCommand: bestScript.command,
|
|
348
|
+
detectedPackageManager: packageManager,
|
|
349
|
+
isMonorepo: isRepo,
|
|
350
|
+
availableScripts,
|
|
351
|
+
suggestedScripts,
|
|
352
|
+
compatibility,
|
|
353
|
+
sizing
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const startCommand = scripts.dev ? `${runCmd} dev` : scripts.start ? `${runCmd} start` : "node index.js";
|
|
357
|
+
return {
|
|
358
|
+
type,
|
|
359
|
+
port: defaultPort,
|
|
360
|
+
installCommand,
|
|
361
|
+
startCommand,
|
|
362
|
+
detectedPackageManager: packageManager,
|
|
363
|
+
isMonorepo: isRepo,
|
|
364
|
+
availableScripts,
|
|
365
|
+
suggestedScripts,
|
|
366
|
+
compatibility,
|
|
367
|
+
sizing
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/utils/file-utils.ts
|
|
372
|
+
import * as fs2 from "fs";
|
|
373
|
+
import * as path2 from "path";
|
|
374
|
+
var ALWAYS_IGNORE = [
|
|
375
|
+
"node_modules",
|
|
376
|
+
".git",
|
|
377
|
+
".next",
|
|
378
|
+
"dist",
|
|
379
|
+
"build",
|
|
380
|
+
".cache",
|
|
381
|
+
".turbo",
|
|
382
|
+
".vercel",
|
|
383
|
+
".DS_Store",
|
|
384
|
+
"*.log"
|
|
385
|
+
];
|
|
386
|
+
var ENV_FILES = [".env", ".env.local", ".env.development", ".env.production"];
|
|
387
|
+
function shouldIgnore(name, includeEnvFiles) {
|
|
388
|
+
const alwaysIgnored = ALWAYS_IGNORE.some((pattern) => {
|
|
389
|
+
if (pattern.startsWith("*")) {
|
|
390
|
+
return name.endsWith(pattern.slice(1));
|
|
391
|
+
}
|
|
392
|
+
return name === pattern;
|
|
393
|
+
});
|
|
394
|
+
if (alwaysIgnored) return true;
|
|
395
|
+
if (!includeEnvFiles && ENV_FILES.some((env) => name === env || name.startsWith(".env"))) {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
function readProjectFiles(projectPath, relativePath = "", includeEnvFiles = false) {
|
|
401
|
+
const files = {};
|
|
402
|
+
const fullPath = path2.join(projectPath, relativePath);
|
|
403
|
+
const entries = fs2.readdirSync(fullPath, { withFileTypes: true });
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
if (shouldIgnore(entry.name, includeEnvFiles)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const entryRelativePath = path2.join(relativePath, entry.name);
|
|
409
|
+
if (entry.isDirectory()) {
|
|
410
|
+
Object.assign(files, readProjectFiles(projectPath, entryRelativePath, includeEnvFiles));
|
|
411
|
+
} else if (entry.isFile()) {
|
|
412
|
+
const filePath = path2.join(projectPath, entryRelativePath);
|
|
413
|
+
try {
|
|
414
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
415
|
+
files[entryRelativePath] = content;
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return files;
|
|
421
|
+
}
|
|
422
|
+
var MAX_CHUNK_SIZE = 500 * 1024;
|
|
423
|
+
var CHUNK_THRESHOLD = 800 * 1024;
|
|
424
|
+
function calculateTotalSize(files) {
|
|
425
|
+
return Object.values(files).reduce((sum, content) => sum + content.length, 0);
|
|
426
|
+
}
|
|
427
|
+
function needsChunkedUpload(files) {
|
|
428
|
+
return calculateTotalSize(files) > CHUNK_THRESHOLD;
|
|
429
|
+
}
|
|
430
|
+
function chunkFiles(files, maxChunkSize = MAX_CHUNK_SIZE) {
|
|
431
|
+
const chunks = [];
|
|
432
|
+
let currentChunk = {};
|
|
433
|
+
let currentSize = 0;
|
|
434
|
+
const entries = Object.entries(files).sort((a, b) => a[1].length - b[1].length);
|
|
435
|
+
for (const [filePath, content] of entries) {
|
|
436
|
+
const fileSize = content.length;
|
|
437
|
+
if (fileSize > maxChunkSize) {
|
|
438
|
+
if (Object.keys(currentChunk).length > 0) {
|
|
439
|
+
chunks.push(currentChunk);
|
|
440
|
+
currentChunk = {};
|
|
441
|
+
currentSize = 0;
|
|
442
|
+
}
|
|
443
|
+
chunks.push({ [filePath]: content });
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (currentSize + fileSize > maxChunkSize && Object.keys(currentChunk).length > 0) {
|
|
447
|
+
chunks.push(currentChunk);
|
|
448
|
+
currentChunk = {};
|
|
449
|
+
currentSize = 0;
|
|
450
|
+
}
|
|
451
|
+
currentChunk[filePath] = content;
|
|
452
|
+
currentSize += fileSize;
|
|
453
|
+
}
|
|
454
|
+
if (Object.keys(currentChunk).length > 0) {
|
|
455
|
+
chunks.push(currentChunk);
|
|
456
|
+
}
|
|
457
|
+
return chunks;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/utils/auth.ts
|
|
461
|
+
import * as http from "http";
|
|
462
|
+
import * as fs3 from "fs";
|
|
463
|
+
import * as path3 from "path";
|
|
464
|
+
import { exec } from "child_process";
|
|
465
|
+
import { promisify } from "util";
|
|
466
|
+
var execAsync = promisify(exec);
|
|
467
|
+
function escapeHtml(value) {
|
|
468
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
469
|
+
}
|
|
470
|
+
var AUTH_FILE = path3.join(
|
|
471
|
+
process.env.HOME || process.env.USERPROFILE || "",
|
|
472
|
+
".claude",
|
|
473
|
+
"mcp-inflight-auth.json"
|
|
474
|
+
);
|
|
475
|
+
var LEGACY_AUTH_FILE = path3.join(
|
|
476
|
+
process.env.HOME || process.env.USERPROFILE || "",
|
|
477
|
+
".claude",
|
|
478
|
+
"mcp-sandbox-auth.json"
|
|
479
|
+
);
|
|
480
|
+
var INFLIGHT_BASE = process.env.INFLIGHT_URL || "https://www.inflight.co";
|
|
481
|
+
function getAuthData() {
|
|
482
|
+
try {
|
|
483
|
+
const content = fs3.readFileSync(AUTH_FILE, "utf-8");
|
|
484
|
+
return JSON.parse(content);
|
|
485
|
+
} catch {
|
|
486
|
+
try {
|
|
487
|
+
const legacyContent = fs3.readFileSync(LEGACY_AUTH_FILE, "utf-8");
|
|
488
|
+
const legacyData = JSON.parse(legacyContent);
|
|
489
|
+
saveAuthData(legacyData);
|
|
490
|
+
try {
|
|
491
|
+
fs3.unlinkSync(LEGACY_AUTH_FILE);
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
return legacyData;
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function saveAuthData(data) {
|
|
501
|
+
const dir = path3.dirname(AUTH_FILE);
|
|
502
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
503
|
+
fs3.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
504
|
+
}
|
|
505
|
+
function clearAuthData() {
|
|
506
|
+
try {
|
|
507
|
+
fs3.unlinkSync(AUTH_FILE);
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
fs3.unlinkSync(LEGACY_AUTH_FILE);
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function isAuthenticated() {
|
|
516
|
+
return getAuthData() !== null;
|
|
517
|
+
}
|
|
518
|
+
async function openBrowser(url) {
|
|
519
|
+
const platform = process.platform;
|
|
520
|
+
try {
|
|
521
|
+
if (platform === "darwin") {
|
|
522
|
+
await execAsync(`open "${url}"`);
|
|
523
|
+
} else if (platform === "win32") {
|
|
524
|
+
await execAsync(`start "" "${url}"`);
|
|
525
|
+
} else {
|
|
526
|
+
await execAsync(`xdg-open "${url}"`);
|
|
527
|
+
}
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
`Failed to open browser. Please manually visit: ${url}`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function authenticate(log) {
|
|
535
|
+
return new Promise((resolve3, reject) => {
|
|
536
|
+
const server = http.createServer((req, res) => {
|
|
537
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
538
|
+
if (url.pathname === "/callback") {
|
|
539
|
+
const apiKey = url.searchParams.get("api_key");
|
|
540
|
+
const userId = url.searchParams.get("user_id");
|
|
541
|
+
const email = url.searchParams.get("email");
|
|
542
|
+
const name = url.searchParams.get("name");
|
|
543
|
+
if (!apiKey || !userId) {
|
|
544
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
545
|
+
res.end(`
|
|
546
|
+
<html>
|
|
547
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
548
|
+
<h1 style="color: #e53e3e;">Authentication Failed</h1>
|
|
549
|
+
<p>Missing API key or user ID. Please try again.</p>
|
|
550
|
+
</body>
|
|
551
|
+
</html>
|
|
552
|
+
`);
|
|
553
|
+
server.close();
|
|
554
|
+
reject(new Error("Missing API key or user ID in callback"));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const displayName = name || email || "there";
|
|
558
|
+
const safeDisplayName = escapeHtml(displayName);
|
|
559
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
560
|
+
res.end(`
|
|
561
|
+
<!DOCTYPE html>
|
|
562
|
+
<html>
|
|
563
|
+
<head>
|
|
564
|
+
<meta charset="utf-8">
|
|
565
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
566
|
+
<title>Authenticated - InFlight</title>
|
|
567
|
+
<style>
|
|
568
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
569
|
+
body {
|
|
570
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
571
|
+
background-color: #15161C;
|
|
572
|
+
color: #F9FAFB;
|
|
573
|
+
min-height: 100vh;
|
|
574
|
+
display: flex;
|
|
575
|
+
align-items: center;
|
|
576
|
+
justify-content: center;
|
|
577
|
+
}
|
|
578
|
+
.container {
|
|
579
|
+
background-color: #0F1012;
|
|
580
|
+
border: 1px solid #1F2025;
|
|
581
|
+
border-radius: 16px;
|
|
582
|
+
padding: 48px;
|
|
583
|
+
text-align: center;
|
|
584
|
+
max-width: 400px;
|
|
585
|
+
width: 90%;
|
|
586
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
|
587
|
+
}
|
|
588
|
+
.logo {
|
|
589
|
+
margin-bottom: 32px;
|
|
590
|
+
}
|
|
591
|
+
.success-icon {
|
|
592
|
+
width: 64px;
|
|
593
|
+
height: 64px;
|
|
594
|
+
background: linear-gradient(135deg, #1C8AF8 0%, #60ADFA 100%);
|
|
595
|
+
border-radius: 50%;
|
|
596
|
+
display: flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
justify-content: center;
|
|
599
|
+
margin: 0 auto 24px;
|
|
600
|
+
}
|
|
601
|
+
.success-icon svg {
|
|
602
|
+
width: 32px;
|
|
603
|
+
height: 32px;
|
|
604
|
+
}
|
|
605
|
+
h1 {
|
|
606
|
+
font-size: 24px;
|
|
607
|
+
font-weight: 600;
|
|
608
|
+
margin-bottom: 8px;
|
|
609
|
+
color: #F9FAFB;
|
|
610
|
+
}
|
|
611
|
+
.greeting {
|
|
612
|
+
font-size: 16px;
|
|
613
|
+
color: #98A1AE;
|
|
614
|
+
margin-bottom: 24px;
|
|
615
|
+
}
|
|
616
|
+
.message {
|
|
617
|
+
font-size: 14px;
|
|
618
|
+
color: #697282;
|
|
619
|
+
line-height: 1.5;
|
|
620
|
+
}
|
|
621
|
+
.close-hint {
|
|
622
|
+
margin-top: 24px;
|
|
623
|
+
padding-top: 24px;
|
|
624
|
+
border-top: 1px solid #1F2025;
|
|
625
|
+
font-size: 13px;
|
|
626
|
+
color: #697282;
|
|
627
|
+
}
|
|
628
|
+
</style>
|
|
629
|
+
</head>
|
|
630
|
+
<body>
|
|
631
|
+
<div class="container">
|
|
632
|
+
<div class="logo">
|
|
633
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="41" height="25" viewBox="0 0 41 25" fill="none">
|
|
634
|
+
<path d="M23.1244 23.6849C22.6097 24.7058 21.0702 24.3421 21.0668 23.1988L21.0015 1.1163C20.9987 0.188062 22.0857 -0.316302 22.7927 0.285127L39.6536 14.6273C40.526 15.3694 39.8055 16.782 38.6925 16.5114L28.8843 14.127C28.3931 14.0076 27.8845 14.2426 27.6569 14.6939L23.1244 23.6849Z" fill="white"/>
|
|
635
|
+
<path d="M16.9597 23.6651C17.4771 24.6846 19.0157 24.3168 19.016 23.1735L19.0223 1.09085C19.0225 0.162606 17.9342 -0.338848 17.2288 0.26447L0.406372 14.6517C-0.464095 15.3961 0.260202 16.8068 1.37245 16.5333L11.1743 14.1226C11.6651 14.0019 12.1744 14.2355 12.4032 14.6862L16.9597 23.6651Z" fill="white"/>
|
|
636
|
+
</svg>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="success-icon">
|
|
639
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="white" stroke-width="3">
|
|
640
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
641
|
+
</svg>
|
|
642
|
+
</div>
|
|
643
|
+
<h1>You're connected!</h1>
|
|
644
|
+
<p class="greeting">Welcome, ${safeDisplayName}</p>
|
|
645
|
+
<p class="message">InFlight is now connected to Claude Code. You can share prototypes and get feedback directly from your terminal.</p>
|
|
646
|
+
<p class="close-hint">This window will close automatically...</p>
|
|
647
|
+
</div>
|
|
648
|
+
<script>setTimeout(() => window.close(), 3000);</script>
|
|
649
|
+
</body>
|
|
650
|
+
</html>
|
|
651
|
+
`);
|
|
652
|
+
const authData = {
|
|
653
|
+
apiKey,
|
|
654
|
+
userId,
|
|
655
|
+
email: email || void 0,
|
|
656
|
+
name: name || void 0,
|
|
657
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
658
|
+
};
|
|
659
|
+
saveAuthData(authData);
|
|
660
|
+
server.close();
|
|
661
|
+
resolve3(authData);
|
|
662
|
+
} else {
|
|
663
|
+
res.writeHead(404);
|
|
664
|
+
res.end("Not found");
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
server.on("error", (err) => {
|
|
668
|
+
reject(new Error(`Failed to start auth server: ${err.message}`));
|
|
669
|
+
});
|
|
670
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
671
|
+
const address = server.address();
|
|
672
|
+
if (!address || typeof address === "string") {
|
|
673
|
+
server.close();
|
|
674
|
+
reject(new Error("Failed to get server address"));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const port = address.port;
|
|
678
|
+
const authUrl = `${INFLIGHT_BASE}/api/mcp/auth?callback_port=${port}`;
|
|
679
|
+
await log("Opening browser for InFlight authentication...");
|
|
680
|
+
await log(`If browser doesn't open, visit: ${authUrl}`);
|
|
681
|
+
try {
|
|
682
|
+
await openBrowser(authUrl);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
await log(
|
|
685
|
+
`Could not open browser automatically. Please visit: ${authUrl}`,
|
|
686
|
+
"warning"
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
const timeout = setTimeout(() => {
|
|
691
|
+
server.close();
|
|
692
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
693
|
+
}, 3e5);
|
|
694
|
+
server.on("close", () => {
|
|
695
|
+
clearTimeout(timeout);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/utils/inflight.ts
|
|
701
|
+
import { spawn } from "child_process";
|
|
702
|
+
var spawnAsync = (command, args) => new Promise((resolve3, reject) => {
|
|
703
|
+
const child = spawn(command, args, { stdio: "ignore" });
|
|
704
|
+
child.on("error", (err) => {
|
|
705
|
+
reject(err);
|
|
706
|
+
});
|
|
707
|
+
child.on("close", () => {
|
|
708
|
+
resolve3();
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
var INFLIGHT_API = process.env.INFLIGHT_URL || "https://www.inflight.co";
|
|
712
|
+
var NotAuthenticatedError = class extends Error {
|
|
713
|
+
constructor() {
|
|
714
|
+
super("Not authenticated with InFlight. Please run authentication first.");
|
|
715
|
+
this.name = "NotAuthenticatedError";
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var APIError = class extends Error {
|
|
719
|
+
status;
|
|
720
|
+
constructor(message, status) {
|
|
721
|
+
super(message);
|
|
722
|
+
this.name = "APIError";
|
|
723
|
+
this.status = status;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
function getAuthHeaders(auth) {
|
|
727
|
+
return {
|
|
728
|
+
Authorization: `Bearer ${auth.apiKey}`,
|
|
729
|
+
"Content-Type": "application/json"
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
async function consumeSSEStream(response, onStatus, onComplete, onError) {
|
|
733
|
+
const reader = response.body?.getReader();
|
|
734
|
+
if (!reader) {
|
|
735
|
+
throw new Error("No response body");
|
|
736
|
+
}
|
|
737
|
+
const decoder = new TextDecoder();
|
|
738
|
+
let buffer = "";
|
|
739
|
+
while (true) {
|
|
740
|
+
const { done, value } = await reader.read();
|
|
741
|
+
if (done) break;
|
|
742
|
+
buffer += decoder.decode(value, { stream: true });
|
|
743
|
+
const lines = buffer.split("\n");
|
|
744
|
+
buffer = lines.pop() || "";
|
|
745
|
+
let currentEvent = "";
|
|
746
|
+
let currentData = "";
|
|
747
|
+
for (const line of lines) {
|
|
748
|
+
if (line.startsWith("event: ")) {
|
|
749
|
+
currentEvent = line.slice(7).trim();
|
|
750
|
+
} else if (line.startsWith("data: ")) {
|
|
751
|
+
currentData = line.slice(6);
|
|
752
|
+
} else if (line === "" && currentEvent && currentData) {
|
|
753
|
+
try {
|
|
754
|
+
const data = JSON.parse(currentData);
|
|
755
|
+
switch (currentEvent) {
|
|
756
|
+
case "status":
|
|
757
|
+
onStatus(data.message);
|
|
758
|
+
break;
|
|
759
|
+
case "complete":
|
|
760
|
+
onComplete(data);
|
|
761
|
+
break;
|
|
762
|
+
case "error":
|
|
763
|
+
onError(data.message);
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
currentEvent = "";
|
|
769
|
+
currentData = "";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function deployProject(files, options, log) {
|
|
775
|
+
const auth = getAuthData();
|
|
776
|
+
if (!auth) {
|
|
777
|
+
throw new NotAuthenticatedError();
|
|
778
|
+
}
|
|
779
|
+
const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/deploy`, {
|
|
780
|
+
method: "POST",
|
|
781
|
+
headers: getAuthHeaders(auth),
|
|
782
|
+
body: JSON.stringify({
|
|
783
|
+
files,
|
|
784
|
+
projectType: options.projectType,
|
|
785
|
+
installCommand: options.installCommand,
|
|
786
|
+
startCommand: options.startCommand,
|
|
787
|
+
port: options.port,
|
|
788
|
+
vmTier: options.vmTier || "Micro",
|
|
789
|
+
projectName: options.projectName,
|
|
790
|
+
workspaceId: options.workspaceId || auth.defaultWorkspaceId,
|
|
791
|
+
gitInfo: options.gitInfo
|
|
792
|
+
})
|
|
793
|
+
});
|
|
794
|
+
if (!response.ok && !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
795
|
+
const text = await response.text();
|
|
796
|
+
throw new APIError(`Failed to deploy: ${text}`, response.status);
|
|
797
|
+
}
|
|
798
|
+
let result = null;
|
|
799
|
+
let errorMessage = null;
|
|
800
|
+
await consumeSSEStream(
|
|
801
|
+
response,
|
|
802
|
+
(message) => log(message),
|
|
803
|
+
(data) => {
|
|
804
|
+
result = data;
|
|
805
|
+
if (result.workspaceId && result.workspaceId !== auth.defaultWorkspaceId) {
|
|
806
|
+
saveAuthData({
|
|
807
|
+
...auth,
|
|
808
|
+
defaultWorkspaceId: result.workspaceId
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
(message) => {
|
|
813
|
+
errorMessage = message;
|
|
814
|
+
}
|
|
815
|
+
);
|
|
816
|
+
if (errorMessage) {
|
|
817
|
+
throw new APIError(errorMessage, 500);
|
|
818
|
+
}
|
|
819
|
+
if (!result) {
|
|
820
|
+
throw new APIError("No response from deploy endpoint", 500);
|
|
821
|
+
}
|
|
822
|
+
return result;
|
|
823
|
+
}
|
|
824
|
+
async function syncProject(sandboxId, files, options, log) {
|
|
825
|
+
const auth = getAuthData();
|
|
826
|
+
if (!auth) {
|
|
827
|
+
throw new NotAuthenticatedError();
|
|
828
|
+
}
|
|
829
|
+
const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/sync`, {
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: getAuthHeaders(auth),
|
|
832
|
+
body: JSON.stringify({
|
|
833
|
+
sandboxId,
|
|
834
|
+
files,
|
|
835
|
+
restartServer: options.restartServer ?? true,
|
|
836
|
+
startCommand: options.startCommand,
|
|
837
|
+
port: options.port,
|
|
838
|
+
gitInfo: options.gitInfo
|
|
839
|
+
})
|
|
840
|
+
});
|
|
841
|
+
if (!response.ok && !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
842
|
+
const text = await response.text();
|
|
843
|
+
throw new APIError(`Failed to sync: ${text}`, response.status);
|
|
844
|
+
}
|
|
845
|
+
let result = null;
|
|
846
|
+
let errorMessage = null;
|
|
847
|
+
await consumeSSEStream(
|
|
848
|
+
response,
|
|
849
|
+
(message) => log(message),
|
|
850
|
+
(data) => {
|
|
851
|
+
result = data;
|
|
852
|
+
},
|
|
853
|
+
(message) => {
|
|
854
|
+
errorMessage = message;
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
if (errorMessage) {
|
|
858
|
+
throw new APIError(errorMessage, 500);
|
|
859
|
+
}
|
|
860
|
+
if (!result) {
|
|
861
|
+
throw new APIError("No response from sync endpoint", 500);
|
|
862
|
+
}
|
|
863
|
+
return result;
|
|
864
|
+
}
|
|
865
|
+
async function listSandboxes(sync = true) {
|
|
866
|
+
const auth = getAuthData();
|
|
867
|
+
if (!auth) {
|
|
868
|
+
throw new NotAuthenticatedError();
|
|
869
|
+
}
|
|
870
|
+
const url = new URL(`${INFLIGHT_API}/api/mcp/sandbox/list`);
|
|
871
|
+
if (sync) {
|
|
872
|
+
url.searchParams.set("sync", "true");
|
|
873
|
+
}
|
|
874
|
+
const response = await fetch(url.toString(), {
|
|
875
|
+
method: "GET",
|
|
876
|
+
headers: getAuthHeaders(auth)
|
|
877
|
+
});
|
|
878
|
+
if (!response.ok) {
|
|
879
|
+
const text = await response.text();
|
|
880
|
+
throw new APIError(`Failed to list sandboxes: ${text}`, response.status);
|
|
881
|
+
}
|
|
882
|
+
const data = await response.json();
|
|
883
|
+
return data.sandboxes || [];
|
|
884
|
+
}
|
|
885
|
+
async function deleteSandbox(sandboxId) {
|
|
886
|
+
const auth = getAuthData();
|
|
887
|
+
if (!auth) {
|
|
888
|
+
throw new NotAuthenticatedError();
|
|
889
|
+
}
|
|
890
|
+
const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/${sandboxId}`, {
|
|
891
|
+
method: "DELETE",
|
|
892
|
+
headers: getAuthHeaders(auth)
|
|
893
|
+
});
|
|
894
|
+
if (!response.ok) {
|
|
895
|
+
const text = await response.text();
|
|
896
|
+
throw new APIError(`Failed to delete sandbox: ${text}`, response.status);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
var TRUSTED_DOMAINS = [
|
|
900
|
+
"inflight.co",
|
|
901
|
+
"www.inflight.co",
|
|
902
|
+
"csb.app"
|
|
903
|
+
// CodeSandbox preview URLs
|
|
904
|
+
];
|
|
905
|
+
function isTrustedHost(hostname) {
|
|
906
|
+
const lowerHost = hostname.toLowerCase();
|
|
907
|
+
return TRUSTED_DOMAINS.some((domain) => {
|
|
908
|
+
return lowerHost === domain || lowerHost.endsWith(`.${domain}`);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
async function openInBrowser(url) {
|
|
912
|
+
if (typeof url !== "string") {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
let parsed;
|
|
916
|
+
try {
|
|
917
|
+
parsed = new URL(url);
|
|
918
|
+
} catch {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (!isTrustedHost(parsed.hostname)) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const safeUrl = parsed.href;
|
|
928
|
+
const platform = process.platform;
|
|
929
|
+
try {
|
|
930
|
+
if (platform === "darwin") {
|
|
931
|
+
await spawnAsync("open", [safeUrl]);
|
|
932
|
+
} else if (platform === "win32") {
|
|
933
|
+
await spawnAsync("cmd", ["/c", "start", "", safeUrl]);
|
|
934
|
+
} else {
|
|
935
|
+
await spawnAsync("xdg-open", [safeUrl]);
|
|
936
|
+
}
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function createSandbox(options, log) {
|
|
941
|
+
const auth = getAuthData();
|
|
942
|
+
if (!auth) {
|
|
943
|
+
throw new NotAuthenticatedError();
|
|
944
|
+
}
|
|
945
|
+
await log("Creating sandbox...");
|
|
946
|
+
const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/create`, {
|
|
947
|
+
method: "POST",
|
|
948
|
+
headers: getAuthHeaders(auth),
|
|
949
|
+
body: JSON.stringify({
|
|
950
|
+
vmTier: options.vmTier || "Micro",
|
|
951
|
+
workspaceId: options.workspaceId || auth.defaultWorkspaceId,
|
|
952
|
+
projectName: options.projectName
|
|
953
|
+
})
|
|
954
|
+
});
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
const data2 = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
957
|
+
throw new APIError(data2.error || "Failed to create sandbox", response.status);
|
|
958
|
+
}
|
|
959
|
+
const data = await response.json();
|
|
960
|
+
if (!data.success) {
|
|
961
|
+
throw new APIError(data.error || "Failed to create sandbox", 500);
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
sandboxId: data.sandboxId,
|
|
965
|
+
workspaceId: data.workspaceId
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
async function uploadChunk(sandboxId, files, chunkIndex, totalChunks, log) {
|
|
969
|
+
const auth = getAuthData();
|
|
970
|
+
if (!auth) {
|
|
971
|
+
throw new NotAuthenticatedError();
|
|
972
|
+
}
|
|
973
|
+
const fileCount = Object.keys(files).length;
|
|
974
|
+
const chunkSize = Object.values(files).reduce((sum, content) => sum + content.length, 0);
|
|
975
|
+
await log(`Uploading chunk ${chunkIndex + 1}/${totalChunks} (${fileCount} files, ${(chunkSize / 1024).toFixed(1)} KB)...`);
|
|
976
|
+
const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/${sandboxId}/upload`, {
|
|
977
|
+
method: "POST",
|
|
978
|
+
headers: getAuthHeaders(auth),
|
|
979
|
+
body: JSON.stringify({
|
|
980
|
+
files,
|
|
981
|
+
chunkIndex,
|
|
982
|
+
totalChunks
|
|
983
|
+
})
|
|
984
|
+
});
|
|
985
|
+
if (!response.ok) {
|
|
986
|
+
const data2 = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
987
|
+
throw new APIError(data2.error || `Failed to upload chunk ${chunkIndex}`, response.status);
|
|
988
|
+
}
|
|
989
|
+
const data = await response.json();
|
|
990
|
+
if (!data.success) {
|
|
991
|
+
throw new APIError(data.error || `Failed to upload chunk ${chunkIndex}`, 500);
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
chunkIndex: data.chunkIndex,
|
|
995
|
+
fileCount: data.fileCount
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
async function finalizeSandbox(sandboxId, options, log) {
|
|
999
|
+
const auth = getAuthData();
|
|
1000
|
+
if (!auth) {
|
|
1001
|
+
throw new NotAuthenticatedError();
|
|
1002
|
+
}
|
|
1003
|
+
await log("Finalizing deployment...");
|
|
1004
|
+
const response = await fetch(`${INFLIGHT_API}/api/mcp/sandbox/${sandboxId}/finalize`, {
|
|
1005
|
+
method: "POST",
|
|
1006
|
+
headers: getAuthHeaders(auth),
|
|
1007
|
+
body: JSON.stringify({
|
|
1008
|
+
projectType: options.projectType,
|
|
1009
|
+
installCommand: options.installCommand,
|
|
1010
|
+
startCommand: options.startCommand,
|
|
1011
|
+
port: options.port,
|
|
1012
|
+
projectName: options.projectName,
|
|
1013
|
+
workspaceId: options.workspaceId,
|
|
1014
|
+
gitInfo: options.gitInfo
|
|
1015
|
+
})
|
|
1016
|
+
});
|
|
1017
|
+
if (!response.ok && !response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
1018
|
+
const text = await response.text();
|
|
1019
|
+
throw new APIError(`Failed to finalize: ${text}`, response.status);
|
|
1020
|
+
}
|
|
1021
|
+
let result = null;
|
|
1022
|
+
let errorMessage = null;
|
|
1023
|
+
await consumeSSEStream(
|
|
1024
|
+
response,
|
|
1025
|
+
(message) => log(message),
|
|
1026
|
+
(data) => {
|
|
1027
|
+
result = data;
|
|
1028
|
+
if (result.workspaceId && result.workspaceId !== auth.defaultWorkspaceId) {
|
|
1029
|
+
saveAuthData({
|
|
1030
|
+
...auth,
|
|
1031
|
+
defaultWorkspaceId: result.workspaceId
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
(message) => {
|
|
1036
|
+
errorMessage = message;
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
if (errorMessage) {
|
|
1040
|
+
throw new APIError(errorMessage, 500);
|
|
1041
|
+
}
|
|
1042
|
+
if (!result) {
|
|
1043
|
+
throw new APIError("No response from finalize endpoint", 500);
|
|
1044
|
+
}
|
|
1045
|
+
return result;
|
|
1046
|
+
}
|
|
1047
|
+
async function deployProjectChunked(fileChunks, options, log) {
|
|
1048
|
+
const { sandboxId, workspaceId } = await createSandbox(
|
|
1049
|
+
{
|
|
1050
|
+
vmTier: options.vmTier,
|
|
1051
|
+
workspaceId: options.workspaceId,
|
|
1052
|
+
projectName: options.projectName
|
|
1053
|
+
},
|
|
1054
|
+
log
|
|
1055
|
+
);
|
|
1056
|
+
await log(`Sandbox created: ${sandboxId}`);
|
|
1057
|
+
const totalChunks = fileChunks.length;
|
|
1058
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
1059
|
+
await uploadChunk(sandboxId, fileChunks[i], i, totalChunks, log);
|
|
1060
|
+
}
|
|
1061
|
+
await log("All files uploaded");
|
|
1062
|
+
const result = await finalizeSandbox(
|
|
1063
|
+
sandboxId,
|
|
1064
|
+
{
|
|
1065
|
+
projectType: options.projectType,
|
|
1066
|
+
installCommand: options.installCommand,
|
|
1067
|
+
startCommand: options.startCommand,
|
|
1068
|
+
port: options.port,
|
|
1069
|
+
projectName: options.projectName,
|
|
1070
|
+
workspaceId,
|
|
1071
|
+
gitInfo: options.gitInfo
|
|
1072
|
+
},
|
|
1073
|
+
log
|
|
1074
|
+
);
|
|
1075
|
+
return result;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/utils/git-utils.ts
|
|
1079
|
+
import { execSync, execFileSync } from "child_process";
|
|
1080
|
+
function gitExec(command, cwd) {
|
|
1081
|
+
try {
|
|
1082
|
+
return execSync(command, {
|
|
1083
|
+
cwd,
|
|
1084
|
+
encoding: "utf-8",
|
|
1085
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1086
|
+
}).trim();
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function isGitRepo(projectPath) {
|
|
1092
|
+
const result = gitExec("git rev-parse --is-inside-work-tree", projectPath);
|
|
1093
|
+
return result === "true";
|
|
1094
|
+
}
|
|
1095
|
+
function hasUncommittedChanges(projectPath) {
|
|
1096
|
+
const status = gitExec("git status --porcelain", projectPath);
|
|
1097
|
+
return status !== null && status.length > 0;
|
|
1098
|
+
}
|
|
1099
|
+
function sanitizeRemoteUrl(url) {
|
|
1100
|
+
if (!url) return null;
|
|
1101
|
+
let cleaned = url.replace(/\.git$/, "");
|
|
1102
|
+
if (cleaned.startsWith("git@")) {
|
|
1103
|
+
cleaned = cleaned.replace(/^git@/, "").replace(":", "/");
|
|
1104
|
+
return cleaned;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
const parsed = new URL(cleaned);
|
|
1108
|
+
return `${parsed.host}${parsed.pathname}`;
|
|
1109
|
+
} catch {
|
|
1110
|
+
return cleaned.replace(/^https?:\/\/[^@]+@/, "").replace(/^https?:\/\//, "");
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function getGitInfo(projectPath) {
|
|
1114
|
+
if (!isGitRepo(projectPath)) {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
const branch = gitExec("git rev-parse --abbrev-ref HEAD", projectPath);
|
|
1118
|
+
const commitShort = gitExec("git rev-parse --short HEAD", projectPath);
|
|
1119
|
+
const commitFull = gitExec("git rev-parse HEAD", projectPath);
|
|
1120
|
+
const commitMessage = gitExec("git log -1 --format=%s", projectPath);
|
|
1121
|
+
const rawRemoteUrl = gitExec("git remote get-url origin", projectPath);
|
|
1122
|
+
const timestamp = gitExec("git log -1 --format=%cI", projectPath);
|
|
1123
|
+
const isDirty = hasUncommittedChanges(projectPath);
|
|
1124
|
+
return {
|
|
1125
|
+
branch,
|
|
1126
|
+
commitShort,
|
|
1127
|
+
commitFull,
|
|
1128
|
+
commitMessage,
|
|
1129
|
+
remoteUrl: sanitizeRemoteUrl(rawRemoteUrl),
|
|
1130
|
+
isDirty,
|
|
1131
|
+
timestamp
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
function autoCommitForShare(projectPath, inflightVersionId) {
|
|
1135
|
+
if (!isGitRepo(projectPath)) {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
if (!hasUncommittedChanges(projectPath)) {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
execSync("git add -A", {
|
|
1143
|
+
cwd: projectPath,
|
|
1144
|
+
encoding: "utf-8",
|
|
1145
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1146
|
+
});
|
|
1147
|
+
const commitMessage = `InFlight share: ${inflightVersionId}`;
|
|
1148
|
+
execFileSync(
|
|
1149
|
+
"git",
|
|
1150
|
+
["commit", "-m", commitMessage],
|
|
1151
|
+
{
|
|
1152
|
+
cwd: projectPath,
|
|
1153
|
+
encoding: "utf-8",
|
|
1154
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
1157
|
+
return gitExec("git rev-parse --short HEAD", projectPath);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/tools/deploy.ts
|
|
1164
|
+
async function deployPrototype(args, log) {
|
|
1165
|
+
const startTime = Date.now();
|
|
1166
|
+
await log(`Starting share for: ${args.path}`);
|
|
1167
|
+
const projectPath = path4.resolve(args.path);
|
|
1168
|
+
if (!fs4.existsSync(projectPath)) {
|
|
1169
|
+
throw new Error(`Project path does not exist: ${projectPath}`);
|
|
1170
|
+
}
|
|
1171
|
+
if (!fs4.statSync(projectPath).isDirectory()) {
|
|
1172
|
+
throw new Error(`Path is not a directory: ${projectPath}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!isAuthenticated()) {
|
|
1175
|
+
await log("Authenticating with InFlight...");
|
|
1176
|
+
try {
|
|
1177
|
+
await authenticate(log);
|
|
1178
|
+
await log("Authenticated with InFlight");
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1181
|
+
throw new Error(`InFlight authentication failed: ${errorMessage}`);
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
await log("Using saved InFlight credentials");
|
|
1185
|
+
}
|
|
1186
|
+
await log("Analyzing project...");
|
|
1187
|
+
const projectInfo = detectProjectType(projectPath);
|
|
1188
|
+
let gitInfo = getGitInfo(projectPath);
|
|
1189
|
+
const hadUncommittedChanges = gitInfo?.isDirty ?? false;
|
|
1190
|
+
if (gitInfo) {
|
|
1191
|
+
const dirtyMarker = gitInfo.isDirty ? " (modified)" : "";
|
|
1192
|
+
await log(`Git: ${gitInfo.branch}@${gitInfo.commitShort}${dirtyMarker}`);
|
|
1193
|
+
}
|
|
1194
|
+
if (!projectInfo.compatibility.canDeploy) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`Project cannot be deployed: ${projectInfo.compatibility.issues.join(", ")}`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
if (projectInfo.compatibility.warnings.length > 0) {
|
|
1200
|
+
await log(`Warnings: ${projectInfo.compatibility.warnings.join(", ")}`, "warning");
|
|
1201
|
+
}
|
|
1202
|
+
if (args.port) {
|
|
1203
|
+
projectInfo.port = args.port;
|
|
1204
|
+
}
|
|
1205
|
+
if (args.command) {
|
|
1206
|
+
projectInfo.startCommand = args.command;
|
|
1207
|
+
}
|
|
1208
|
+
const isMonorepo2 = projectInfo.isMonorepo ? " (monorepo)" : "";
|
|
1209
|
+
await log(`Detected: ${projectInfo.type}${isMonorepo2}`);
|
|
1210
|
+
await log(`Package manager: ${projectInfo.detectedPackageManager}`);
|
|
1211
|
+
await log(`Install command: ${projectInfo.installCommand || "none"}`);
|
|
1212
|
+
await log(`Start command: ${projectInfo.startCommand}`);
|
|
1213
|
+
await log(`Port: ${projectInfo.port}`);
|
|
1214
|
+
await log(`VM tier: ${projectInfo.sizing.recommendedTier}`);
|
|
1215
|
+
await log("Reading project files...");
|
|
1216
|
+
const includeEnv = args.includeEnvFiles ?? false;
|
|
1217
|
+
if (includeEnv) {
|
|
1218
|
+
await log("Including .env files as requested");
|
|
1219
|
+
}
|
|
1220
|
+
const files = readProjectFiles(projectPath, "", includeEnv);
|
|
1221
|
+
const fileCount = Object.keys(files).length;
|
|
1222
|
+
const totalSize = Object.values(files).reduce((sum, content) => sum + content.length, 0);
|
|
1223
|
+
await log(`Found ${fileCount} files (${(totalSize / 1024).toFixed(1)} KB)`);
|
|
1224
|
+
if (fileCount === 0) {
|
|
1225
|
+
throw new Error("No files found in project directory");
|
|
1226
|
+
}
|
|
1227
|
+
let existingSandbox;
|
|
1228
|
+
try {
|
|
1229
|
+
const sandboxes = await listSandboxes();
|
|
1230
|
+
const projectName = path4.basename(projectPath);
|
|
1231
|
+
existingSandbox = sandboxes.find(
|
|
1232
|
+
(s) => s.projectName === projectName && s.status === "running"
|
|
1233
|
+
);
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
let result;
|
|
1237
|
+
if (existingSandbox) {
|
|
1238
|
+
await log(`Found existing sandbox: ${existingSandbox.sandboxId}`);
|
|
1239
|
+
await log("Syncing files...");
|
|
1240
|
+
try {
|
|
1241
|
+
const syncResult = await syncProject(
|
|
1242
|
+
existingSandbox.sandboxId,
|
|
1243
|
+
files,
|
|
1244
|
+
{
|
|
1245
|
+
restartServer: true,
|
|
1246
|
+
startCommand: projectInfo.startCommand,
|
|
1247
|
+
port: projectInfo.port,
|
|
1248
|
+
gitInfo: gitInfo || void 0
|
|
1249
|
+
},
|
|
1250
|
+
log
|
|
1251
|
+
);
|
|
1252
|
+
const inflightUrl = existingSandbox.inflightUrl || "";
|
|
1253
|
+
const inflightVersionId = existingSandbox.inflightVersionId || "";
|
|
1254
|
+
result = {
|
|
1255
|
+
url: syncResult.sandboxUrl,
|
|
1256
|
+
sandboxId: existingSandbox.sandboxId,
|
|
1257
|
+
projectType: projectInfo.type,
|
|
1258
|
+
synced: true,
|
|
1259
|
+
inflightUrl,
|
|
1260
|
+
inflightVersionId
|
|
1261
|
+
};
|
|
1262
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
1263
|
+
await log(`Sync complete in ${elapsed}s`);
|
|
1264
|
+
} catch (syncError) {
|
|
1265
|
+
const errorMsg = syncError instanceof Error ? syncError.message : String(syncError);
|
|
1266
|
+
await log(`Sync failed, creating new sandbox: ${errorMsg}`, "warning");
|
|
1267
|
+
existingSandbox = void 0;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (!existingSandbox) {
|
|
1271
|
+
const useChunkedUpload = needsChunkedUpload(files);
|
|
1272
|
+
if (useChunkedUpload) {
|
|
1273
|
+
const totalSize2 = calculateTotalSize(files);
|
|
1274
|
+
await log(`Large project detected (${(totalSize2 / 1024 / 1024).toFixed(1)} MB), using chunked upload...`);
|
|
1275
|
+
const fileChunks = chunkFiles(files);
|
|
1276
|
+
await log(`Split into ${fileChunks.length} chunks`);
|
|
1277
|
+
const deployResult = await deployProjectChunked(
|
|
1278
|
+
fileChunks,
|
|
1279
|
+
{
|
|
1280
|
+
projectType: projectInfo.type,
|
|
1281
|
+
installCommand: projectInfo.installCommand || void 0,
|
|
1282
|
+
startCommand: projectInfo.startCommand,
|
|
1283
|
+
port: projectInfo.port,
|
|
1284
|
+
vmTier: projectInfo.sizing.recommendedTier,
|
|
1285
|
+
projectName: path4.basename(projectPath),
|
|
1286
|
+
workspaceId: args.workspaceId,
|
|
1287
|
+
gitInfo: gitInfo || void 0
|
|
1288
|
+
},
|
|
1289
|
+
log
|
|
1290
|
+
);
|
|
1291
|
+
result = {
|
|
1292
|
+
url: deployResult.sandboxUrl,
|
|
1293
|
+
sandboxId: deployResult.sandboxId,
|
|
1294
|
+
projectType: projectInfo.type,
|
|
1295
|
+
synced: false,
|
|
1296
|
+
inflightUrl: deployResult.inflightUrl,
|
|
1297
|
+
inflightVersionId: deployResult.versionId
|
|
1298
|
+
};
|
|
1299
|
+
} else {
|
|
1300
|
+
await log("Deploying via InFlight API...");
|
|
1301
|
+
const deployResult = await deployProject(
|
|
1302
|
+
files,
|
|
1303
|
+
{
|
|
1304
|
+
projectType: projectInfo.type,
|
|
1305
|
+
installCommand: projectInfo.installCommand || void 0,
|
|
1306
|
+
startCommand: projectInfo.startCommand,
|
|
1307
|
+
port: projectInfo.port,
|
|
1308
|
+
vmTier: projectInfo.sizing.recommendedTier,
|
|
1309
|
+
projectName: path4.basename(projectPath),
|
|
1310
|
+
workspaceId: args.workspaceId,
|
|
1311
|
+
gitInfo: gitInfo || void 0
|
|
1312
|
+
},
|
|
1313
|
+
log
|
|
1314
|
+
);
|
|
1315
|
+
result = {
|
|
1316
|
+
url: deployResult.sandboxUrl,
|
|
1317
|
+
sandboxId: deployResult.sandboxId,
|
|
1318
|
+
projectType: projectInfo.type,
|
|
1319
|
+
synced: false,
|
|
1320
|
+
inflightUrl: deployResult.inflightUrl,
|
|
1321
|
+
inflightVersionId: deployResult.versionId
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
1325
|
+
await log(`Deploy complete in ${elapsed}s`);
|
|
1326
|
+
}
|
|
1327
|
+
if (hadUncommittedChanges && result.inflightVersionId) {
|
|
1328
|
+
const newCommit = autoCommitForShare(projectPath, result.inflightVersionId);
|
|
1329
|
+
if (newCommit) {
|
|
1330
|
+
await log(`Auto-committed changes: ${newCommit}`);
|
|
1331
|
+
gitInfo = getGitInfo(projectPath);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
await log(`InFlight: ${result.inflightUrl}`);
|
|
1335
|
+
if (args.openBrowser !== false && result.inflightUrl) {
|
|
1336
|
+
await openInBrowser(result.inflightUrl);
|
|
1337
|
+
await log("Opened InFlight in browser");
|
|
1338
|
+
}
|
|
1339
|
+
return result;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/server.ts
|
|
1343
|
+
import * as path5 from "path";
|
|
1344
|
+
import * as fs5 from "fs";
|
|
1345
|
+
function createServer2() {
|
|
1346
|
+
const server = new Server(
|
|
1347
|
+
{
|
|
1348
|
+
name: "mcp-inflight",
|
|
1349
|
+
version: "0.2.0"
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
capabilities: {
|
|
1353
|
+
tools: {},
|
|
1354
|
+
logging: {}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
const log = async (message, level = "info") => {
|
|
1359
|
+
try {
|
|
1360
|
+
await server.sendLoggingMessage({
|
|
1361
|
+
level,
|
|
1362
|
+
data: message,
|
|
1363
|
+
logger: "mcp-inflight"
|
|
1364
|
+
});
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1369
|
+
return {
|
|
1370
|
+
tools: [
|
|
1371
|
+
{
|
|
1372
|
+
name: "share",
|
|
1373
|
+
description: "Share a local project by deploying it to a public URL and creating an InFlight version for feedback. Supports React, Next.js, Vite, Node.js, and static HTML projects. Returns the InFlight URL. Requires InFlight authentication (will prompt if not authenticated).",
|
|
1374
|
+
inputSchema: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: {
|
|
1377
|
+
path: {
|
|
1378
|
+
type: "string",
|
|
1379
|
+
description: "Absolute path to the project directory to share"
|
|
1380
|
+
},
|
|
1381
|
+
port: {
|
|
1382
|
+
type: "number",
|
|
1383
|
+
description: "Port the dev server runs on (optional, auto-detected based on project type)"
|
|
1384
|
+
},
|
|
1385
|
+
command: {
|
|
1386
|
+
type: "string",
|
|
1387
|
+
description: "Custom start command (optional, overrides auto-detection). Example: 'npm run dev' or 'node server.js'"
|
|
1388
|
+
},
|
|
1389
|
+
includeEnvFiles: {
|
|
1390
|
+
type: "boolean",
|
|
1391
|
+
description: "Whether to include .env files in the deployment. Default is false for security. Only set to true if the user explicitly confirms they want to include environment variables."
|
|
1392
|
+
},
|
|
1393
|
+
openBrowser: {
|
|
1394
|
+
type: "boolean",
|
|
1395
|
+
description: "Open the InFlight URL in the browser after deployment. Default is true."
|
|
1396
|
+
},
|
|
1397
|
+
workspaceId: {
|
|
1398
|
+
type: "string",
|
|
1399
|
+
description: "InFlight workspace ID to create the version in. If not provided, uses the last-used workspace or the user's first workspace."
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
required: ["path"]
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
{
|
|
1406
|
+
name: "prototype_sync",
|
|
1407
|
+
description: "Sync/update files in an existing prototype without recreating it. Restarts the server for changes to take effect. Use this when you've made changes to an already-shared project.",
|
|
1408
|
+
inputSchema: {
|
|
1409
|
+
type: "object",
|
|
1410
|
+
properties: {
|
|
1411
|
+
prototypeId: {
|
|
1412
|
+
type: "string",
|
|
1413
|
+
description: "The prototype ID to sync files to (from a previous share)"
|
|
1414
|
+
},
|
|
1415
|
+
path: {
|
|
1416
|
+
type: "string",
|
|
1417
|
+
description: "Absolute path to the project directory"
|
|
1418
|
+
},
|
|
1419
|
+
includeEnvFiles: {
|
|
1420
|
+
type: "boolean",
|
|
1421
|
+
description: "Whether to include .env files. Default is false."
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
required: ["prototypeId", "path"]
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
name: "prototype_list",
|
|
1429
|
+
description: "List all prototypes that have been shared by the current user. Shows project info and associated InFlight URLs.",
|
|
1430
|
+
inputSchema: {
|
|
1431
|
+
type: "object",
|
|
1432
|
+
properties: {}
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
name: "prototype_delete",
|
|
1437
|
+
description: "Delete a shared prototype. This will stop the prototype and make it unavailable.",
|
|
1438
|
+
inputSchema: {
|
|
1439
|
+
type: "object",
|
|
1440
|
+
properties: {
|
|
1441
|
+
prototypeId: {
|
|
1442
|
+
type: "string",
|
|
1443
|
+
description: "The prototype ID to delete"
|
|
1444
|
+
}
|
|
1445
|
+
},
|
|
1446
|
+
required: ["prototypeId"]
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
name: "login",
|
|
1451
|
+
description: "Log in to InFlight. If already logged in, shows current user info. Opens browser for authentication if not logged in.",
|
|
1452
|
+
inputSchema: {
|
|
1453
|
+
type: "object",
|
|
1454
|
+
properties: {}
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
name: "logout",
|
|
1459
|
+
description: "Log out of InFlight. Clears stored authentication credentials.",
|
|
1460
|
+
inputSchema: {
|
|
1461
|
+
type: "object",
|
|
1462
|
+
properties: {}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
]
|
|
1466
|
+
};
|
|
1467
|
+
});
|
|
1468
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1469
|
+
const { name, arguments: args } = request.params;
|
|
1470
|
+
try {
|
|
1471
|
+
if (name === "share") {
|
|
1472
|
+
const deployArgs = args;
|
|
1473
|
+
const result = await deployPrototype(deployArgs, log);
|
|
1474
|
+
const message = result.synced ? `Files synced. InFlight: ${result.inflightUrl}` : `Project shared successfully. InFlight: ${result.inflightUrl}`;
|
|
1475
|
+
return {
|
|
1476
|
+
content: [
|
|
1477
|
+
{
|
|
1478
|
+
type: "text",
|
|
1479
|
+
text: JSON.stringify(
|
|
1480
|
+
{
|
|
1481
|
+
success: true,
|
|
1482
|
+
projectType: result.projectType,
|
|
1483
|
+
synced: result.synced ?? false,
|
|
1484
|
+
inflightUrl: result.inflightUrl,
|
|
1485
|
+
inflightVersionId: result.inflightVersionId,
|
|
1486
|
+
message
|
|
1487
|
+
},
|
|
1488
|
+
null,
|
|
1489
|
+
2
|
|
1490
|
+
)
|
|
1491
|
+
}
|
|
1492
|
+
]
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
if (name === "prototype_sync") {
|
|
1496
|
+
const { prototypeId, path: projectPath, includeEnvFiles } = args;
|
|
1497
|
+
if (!isAuthenticated()) {
|
|
1498
|
+
await log("Authenticating with InFlight...");
|
|
1499
|
+
await authenticate(log);
|
|
1500
|
+
}
|
|
1501
|
+
const resolvedPath = path5.resolve(projectPath);
|
|
1502
|
+
if (!fs5.existsSync(resolvedPath)) {
|
|
1503
|
+
throw new Error(`Project path does not exist: ${resolvedPath}`);
|
|
1504
|
+
}
|
|
1505
|
+
const files = readProjectFiles(resolvedPath, "", includeEnvFiles ?? false);
|
|
1506
|
+
const projectInfo = detectProjectType(resolvedPath);
|
|
1507
|
+
await log(`Syncing ${Object.keys(files).length} files to prototype ${prototypeId}...`);
|
|
1508
|
+
const syncResult = await syncProject(
|
|
1509
|
+
prototypeId,
|
|
1510
|
+
files,
|
|
1511
|
+
{
|
|
1512
|
+
restartServer: true,
|
|
1513
|
+
startCommand: projectInfo.startCommand,
|
|
1514
|
+
port: projectInfo.port
|
|
1515
|
+
},
|
|
1516
|
+
log
|
|
1517
|
+
);
|
|
1518
|
+
return {
|
|
1519
|
+
content: [
|
|
1520
|
+
{
|
|
1521
|
+
type: "text",
|
|
1522
|
+
text: JSON.stringify(
|
|
1523
|
+
{
|
|
1524
|
+
success: true,
|
|
1525
|
+
prototypeId,
|
|
1526
|
+
fileCount: syncResult.fileCount,
|
|
1527
|
+
message: `Synced ${syncResult.fileCount} files and restarted server.`
|
|
1528
|
+
},
|
|
1529
|
+
null,
|
|
1530
|
+
2
|
|
1531
|
+
)
|
|
1532
|
+
}
|
|
1533
|
+
]
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (name === "prototype_list") {
|
|
1537
|
+
if (!isAuthenticated()) {
|
|
1538
|
+
await log("Authenticating with InFlight...");
|
|
1539
|
+
await authenticate(log);
|
|
1540
|
+
}
|
|
1541
|
+
await log("Fetching prototypes...");
|
|
1542
|
+
const prototypes = await listSandboxes();
|
|
1543
|
+
return {
|
|
1544
|
+
content: [
|
|
1545
|
+
{
|
|
1546
|
+
type: "text",
|
|
1547
|
+
text: JSON.stringify(
|
|
1548
|
+
{
|
|
1549
|
+
success: true,
|
|
1550
|
+
prototypes: prototypes.map((p) => ({
|
|
1551
|
+
prototypeId: p.sandboxId,
|
|
1552
|
+
projectName: p.projectName,
|
|
1553
|
+
projectType: p.projectType,
|
|
1554
|
+
status: p.status,
|
|
1555
|
+
inflightUrl: p.inflightUrl,
|
|
1556
|
+
createdAt: p.createdAt,
|
|
1557
|
+
updatedAt: p.updatedAt
|
|
1558
|
+
})),
|
|
1559
|
+
message: `Found ${prototypes.length} prototypes.`
|
|
1560
|
+
},
|
|
1561
|
+
null,
|
|
1562
|
+
2
|
|
1563
|
+
)
|
|
1564
|
+
}
|
|
1565
|
+
]
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
if (name === "prototype_delete") {
|
|
1569
|
+
const { prototypeId } = args;
|
|
1570
|
+
if (!isAuthenticated()) {
|
|
1571
|
+
await log("Authenticating with InFlight...");
|
|
1572
|
+
await authenticate(log);
|
|
1573
|
+
}
|
|
1574
|
+
await log(`Deleting prototype ${prototypeId}...`);
|
|
1575
|
+
await deleteSandbox(prototypeId);
|
|
1576
|
+
return {
|
|
1577
|
+
content: [
|
|
1578
|
+
{
|
|
1579
|
+
type: "text",
|
|
1580
|
+
text: JSON.stringify(
|
|
1581
|
+
{
|
|
1582
|
+
success: true,
|
|
1583
|
+
prototypeId,
|
|
1584
|
+
message: `Prototype ${prototypeId} has been deleted.`
|
|
1585
|
+
},
|
|
1586
|
+
null,
|
|
1587
|
+
2
|
|
1588
|
+
)
|
|
1589
|
+
}
|
|
1590
|
+
]
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
if (name === "login") {
|
|
1594
|
+
const authData = getAuthData();
|
|
1595
|
+
if (authData) {
|
|
1596
|
+
const displayName2 = authData.name || authData.email || authData.userId;
|
|
1597
|
+
return {
|
|
1598
|
+
content: [
|
|
1599
|
+
{
|
|
1600
|
+
type: "text",
|
|
1601
|
+
text: JSON.stringify(
|
|
1602
|
+
{
|
|
1603
|
+
success: true,
|
|
1604
|
+
loggedIn: true,
|
|
1605
|
+
name: authData.name || void 0,
|
|
1606
|
+
email: authData.email || void 0,
|
|
1607
|
+
userId: authData.userId,
|
|
1608
|
+
authenticatedAt: authData.createdAt,
|
|
1609
|
+
message: `Logged in as ${displayName2}`
|
|
1610
|
+
},
|
|
1611
|
+
null,
|
|
1612
|
+
2
|
|
1613
|
+
)
|
|
1614
|
+
}
|
|
1615
|
+
]
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
await log("Opening browser for InFlight authentication...");
|
|
1619
|
+
const newAuthData = await authenticate(log);
|
|
1620
|
+
const displayName = newAuthData.name || newAuthData.email || newAuthData.userId;
|
|
1621
|
+
return {
|
|
1622
|
+
content: [
|
|
1623
|
+
{
|
|
1624
|
+
type: "text",
|
|
1625
|
+
text: JSON.stringify(
|
|
1626
|
+
{
|
|
1627
|
+
success: true,
|
|
1628
|
+
loggedIn: true,
|
|
1629
|
+
name: newAuthData.name || void 0,
|
|
1630
|
+
email: newAuthData.email || void 0,
|
|
1631
|
+
userId: newAuthData.userId,
|
|
1632
|
+
authenticatedAt: newAuthData.createdAt,
|
|
1633
|
+
message: `Successfully logged in as ${displayName}`
|
|
1634
|
+
},
|
|
1635
|
+
null,
|
|
1636
|
+
2
|
|
1637
|
+
)
|
|
1638
|
+
}
|
|
1639
|
+
]
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
if (name === "logout") {
|
|
1643
|
+
const wasAuthenticated = isAuthenticated();
|
|
1644
|
+
clearAuthData();
|
|
1645
|
+
return {
|
|
1646
|
+
content: [
|
|
1647
|
+
{
|
|
1648
|
+
type: "text",
|
|
1649
|
+
text: JSON.stringify(
|
|
1650
|
+
{
|
|
1651
|
+
success: true,
|
|
1652
|
+
message: wasAuthenticated ? "Logged out of InFlight successfully." : "Already logged out."
|
|
1653
|
+
},
|
|
1654
|
+
null,
|
|
1655
|
+
2
|
|
1656
|
+
)
|
|
1657
|
+
}
|
|
1658
|
+
]
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
content: [
|
|
1663
|
+
{
|
|
1664
|
+
type: "text",
|
|
1665
|
+
text: `Unknown tool: ${name}`
|
|
1666
|
+
}
|
|
1667
|
+
],
|
|
1668
|
+
isError: true
|
|
1669
|
+
};
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1672
|
+
await log(`Error: ${errorMessage}`, "error");
|
|
1673
|
+
return {
|
|
1674
|
+
content: [
|
|
1675
|
+
{
|
|
1676
|
+
type: "text",
|
|
1677
|
+
text: JSON.stringify(
|
|
1678
|
+
{
|
|
1679
|
+
success: false,
|
|
1680
|
+
error: errorMessage
|
|
1681
|
+
},
|
|
1682
|
+
null,
|
|
1683
|
+
2
|
|
1684
|
+
)
|
|
1685
|
+
}
|
|
1686
|
+
],
|
|
1687
|
+
isError: true
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
return server;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/index.ts
|
|
1695
|
+
async function main() {
|
|
1696
|
+
const server = createServer2();
|
|
1697
|
+
const transport = new StdioServerTransport();
|
|
1698
|
+
await server.connect(transport);
|
|
1699
|
+
process.on("SIGINT", async () => {
|
|
1700
|
+
await server.close();
|
|
1701
|
+
process.exit(0);
|
|
1702
|
+
});
|
|
1703
|
+
process.on("SIGTERM", async () => {
|
|
1704
|
+
await server.close();
|
|
1705
|
+
process.exit(0);
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
main().catch((error) => {
|
|
1709
|
+
console.error("Fatal error:", error);
|
|
1710
|
+
process.exit(1);
|
|
1711
|
+
});
|