mcp-inflight 0.2.0 → 0.2.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/README.md +22 -79
- package/dist/index.js +777 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,107 +1,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mcp-inflight
|
|
2
2
|
|
|
3
|
-
MCP server for sharing local prototypes via
|
|
3
|
+
MCP server for sharing local prototypes via [InFlight](https://www.inflight.co).
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
1. Takes a local project directory
|
|
10
|
-
2. Uploads it to a CodeSandbox VM
|
|
11
|
-
3. Installs dependencies and starts the dev server
|
|
12
|
-
4. Returns a public URL where the app is running
|
|
13
|
-
|
|
14
|
-
Supports React, Next.js, Vite, Node.js, and static HTML projects.
|
|
7
|
+
Share your local projects as live prototypes that stakeholders can interact with and provide feedback on - directly from Claude Code.
|
|
15
8
|
|
|
16
9
|
## Setup
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
1. Go to https://codesandbox.io and sign in (or create account)
|
|
21
|
-
2. Navigate to https://codesandbox.io/t/api
|
|
22
|
-
3. Click "Create Token"
|
|
23
|
-
4. Copy the token
|
|
24
|
-
|
|
25
|
-
### 2. Configure Claude Code
|
|
26
|
-
|
|
27
|
-
Add to your Claude Code MCP config (`~/.claude/claude_code_config.json`):
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"mcpServers": {
|
|
32
|
-
"sandbox": {
|
|
33
|
-
"command": "node",
|
|
34
|
-
"args": ["/path/to/inflight/packages/mcp-sandbox/dist/index.js"],
|
|
35
|
-
"env": {
|
|
36
|
-
"CSB_API_KEY": "your-api-key-here"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Or if published to npm:
|
|
11
|
+
Add to your Claude Code config (`~/.claude.json`):
|
|
44
12
|
|
|
45
13
|
```json
|
|
46
14
|
{
|
|
47
15
|
"mcpServers": {
|
|
48
|
-
"
|
|
16
|
+
"inflight": {
|
|
49
17
|
"command": "npx",
|
|
50
|
-
"args": ["
|
|
51
|
-
"env": {
|
|
52
|
-
"CSB_API_KEY": "your-api-key-here"
|
|
53
|
-
}
|
|
18
|
+
"args": ["-y", "mcp-inflight"]
|
|
54
19
|
}
|
|
55
20
|
}
|
|
56
21
|
}
|
|
57
22
|
```
|
|
58
23
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Once configured, Claude Code can share prototypes:
|
|
62
|
-
|
|
63
|
-
```text
|
|
64
|
-
User: "Share this React app"
|
|
24
|
+
Then restart Claude Code.
|
|
65
25
|
|
|
66
|
-
|
|
26
|
+
## Usage
|
|
67
27
|
|
|
68
|
-
|
|
28
|
+
Use `/share` to share your current project:
|
|
69
29
|
|
|
70
|
-
Your project is live at: https://abc123-3000.csb.app
|
|
71
30
|
```
|
|
31
|
+
User: /share
|
|
72
32
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Parameter | Type | Required | Description |
|
|
78
|
-
|-----------|------|----------|-------------|
|
|
79
|
-
| `path` | string | Yes | Absolute path to the project directory |
|
|
80
|
-
| `port` | number | No | Port the dev server runs on (auto-detected) |
|
|
81
|
-
| `command` | string | No | Custom start command (overrides auto-detection) |
|
|
82
|
-
|
|
83
|
-
### Project Type Detection
|
|
84
|
-
|
|
85
|
-
The tool automatically detects project types:
|
|
33
|
+
Claude: Sharing your project...
|
|
34
|
+
Your prototype is live at: https://www.inflight.co/v/xyz789
|
|
35
|
+
```
|
|
86
36
|
|
|
87
|
-
|
|
88
|
-
- **Vite**: Has `vite` in dependencies → port 5173, `npm run dev`
|
|
89
|
-
- **Create React App**: Has `react-scripts` → port 3000, `npm start`
|
|
90
|
-
- **Node.js**: Has `express`/`fastify`/etc → port 3000, `npm start`
|
|
91
|
-
- **Static**: Has `index.html` without package.json → port 3000, `npx serve .`
|
|
37
|
+
## Commands
|
|
92
38
|
|
|
93
|
-
|
|
39
|
+
- `/share` - Share current project as a live prototype
|
|
40
|
+
- `/sandbox-list` - List your deployed prototypes
|
|
41
|
+
- `/sandbox-sync` - Sync changes to an existing prototype
|
|
42
|
+
- `/sandbox-delete` - Delete a prototype
|
|
94
43
|
|
|
95
|
-
|
|
96
|
-
# Install dependencies
|
|
97
|
-
pnpm install
|
|
44
|
+
## Supported Projects
|
|
98
45
|
|
|
99
|
-
|
|
100
|
-
pnpm build
|
|
46
|
+
Next.js, Vite, Create React App, Node.js, and static HTML.
|
|
101
47
|
|
|
102
|
-
|
|
103
|
-
pnpm typecheck
|
|
48
|
+
## License
|
|
104
49
|
|
|
105
|
-
|
|
106
|
-
pnpm dev
|
|
107
|
-
```
|
|
50
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
|
|
13
13
|
// src/tools/deploy.ts
|
|
14
|
-
import * as
|
|
15
|
-
import * as
|
|
14
|
+
import * as path5 from "path";
|
|
15
|
+
import * as fs5 from "fs";
|
|
16
16
|
|
|
17
17
|
// src/utils/detect-project.ts
|
|
18
18
|
import * as fs from "fs";
|
|
@@ -46,6 +46,127 @@ function isMonorepo(projectPath, pkg) {
|
|
|
46
46
|
(config) => fs.existsSync(path.join(projectPath, config))
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
+
function getWorkspacePatterns(projectPath, pkg) {
|
|
50
|
+
if (pkg.workspaces) {
|
|
51
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
52
|
+
return pkg.workspaces;
|
|
53
|
+
}
|
|
54
|
+
if (pkg.workspaces.packages) {
|
|
55
|
+
return pkg.workspaces.packages;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const pnpmWorkspacePath = path.join(projectPath, "pnpm-workspace.yaml");
|
|
59
|
+
if (fs.existsSync(pnpmWorkspacePath)) {
|
|
60
|
+
try {
|
|
61
|
+
const content = fs.readFileSync(pnpmWorkspacePath, "utf-8");
|
|
62
|
+
const packagesMatch = content.match(/packages:\s*\n((?:\s+-\s+['"]?[^\n]+['"]?\n?)+)/);
|
|
63
|
+
if (packagesMatch) {
|
|
64
|
+
const patterns = packagesMatch[1].split("\n").map((line) => line.trim().replace(/^-\s+['"]?|['"]?$/g, "")).filter(Boolean);
|
|
65
|
+
return patterns;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return ["apps/*", "packages/*"];
|
|
71
|
+
}
|
|
72
|
+
function expandWorkspacePattern(projectPath, pattern) {
|
|
73
|
+
const results = [];
|
|
74
|
+
if (pattern.endsWith("/*")) {
|
|
75
|
+
const basePath = pattern.slice(0, -2);
|
|
76
|
+
const fullBasePath = path.join(projectPath, basePath);
|
|
77
|
+
if (fs.existsSync(fullBasePath)) {
|
|
78
|
+
try {
|
|
79
|
+
const entries = fs.readdirSync(fullBasePath, { withFileTypes: true });
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
82
|
+
const pkgPath = path.join(fullBasePath, entry.name, "package.json");
|
|
83
|
+
if (fs.existsSync(pkgPath)) {
|
|
84
|
+
results.push(path.join(basePath, entry.name));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else if (!pattern.includes("*")) {
|
|
92
|
+
const fullPath = path.join(projectPath, pattern);
|
|
93
|
+
if (fs.existsSync(path.join(fullPath, "package.json"))) {
|
|
94
|
+
results.push(pattern);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
99
|
+
function detectWorkspaceType(workspacePath) {
|
|
100
|
+
const pkgPath = path.join(workspacePath, "package.json");
|
|
101
|
+
if (!fs.existsSync(pkgPath)) {
|
|
102
|
+
return { type: "node", port: 3e3, startCommand: "npm run dev", hasDevScript: false };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
106
|
+
const scripts = pkg.scripts ?? {};
|
|
107
|
+
const hasDevScript = !!scripts.dev || !!scripts.start;
|
|
108
|
+
let type = "node";
|
|
109
|
+
let port = 3e3;
|
|
110
|
+
if (hasDependency(pkg, "next")) {
|
|
111
|
+
type = "nextjs";
|
|
112
|
+
port = 3e3;
|
|
113
|
+
} else if (hasDependency(pkg, "vite")) {
|
|
114
|
+
type = "vite";
|
|
115
|
+
port = 5173;
|
|
116
|
+
} else if (hasDependency(pkg, "react-scripts")) {
|
|
117
|
+
type = "cra";
|
|
118
|
+
port = 3e3;
|
|
119
|
+
}
|
|
120
|
+
if (scripts.dev) {
|
|
121
|
+
const extractedPort = extractPortFromScript(scripts.dev);
|
|
122
|
+
if (extractedPort) port = extractedPort;
|
|
123
|
+
}
|
|
124
|
+
const startCommand = scripts.dev ? "dev" : scripts.start ? "start" : "dev";
|
|
125
|
+
return { type, port, startCommand, hasDevScript };
|
|
126
|
+
} catch {
|
|
127
|
+
return { type: "node", port: 3e3, startCommand: "dev", hasDevScript: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function discoverMonorepoWorkspaces(projectPath, pkg) {
|
|
131
|
+
const patterns = getWorkspacePatterns(projectPath, pkg);
|
|
132
|
+
const workspaces = [];
|
|
133
|
+
for (const pattern of patterns) {
|
|
134
|
+
const expandedPaths = expandWorkspacePattern(projectPath, pattern);
|
|
135
|
+
for (const relativePath of expandedPaths) {
|
|
136
|
+
const fullPath = path.join(projectPath, relativePath);
|
|
137
|
+
const info = detectWorkspaceType(fullPath);
|
|
138
|
+
const pkgPath = path.join(fullPath, "package.json");
|
|
139
|
+
let name = path.basename(relativePath);
|
|
140
|
+
try {
|
|
141
|
+
const pkgContent = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
142
|
+
if (pkgContent.name) {
|
|
143
|
+
name = pkgContent.name;
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
workspaces.push({
|
|
148
|
+
name,
|
|
149
|
+
path: relativePath,
|
|
150
|
+
type: info.type,
|
|
151
|
+
port: info.port,
|
|
152
|
+
startCommand: info.startCommand,
|
|
153
|
+
hasDevScript: info.hasDevScript
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
workspaces.sort((a, b) => {
|
|
158
|
+
if (a.hasDevScript !== b.hasDevScript) {
|
|
159
|
+
return a.hasDevScript ? -1 : 1;
|
|
160
|
+
}
|
|
161
|
+
const aIsApp = a.path.startsWith("apps/");
|
|
162
|
+
const bIsApp = b.path.startsWith("apps/");
|
|
163
|
+
if (aIsApp !== bIsApp) {
|
|
164
|
+
return aIsApp ? -1 : 1;
|
|
165
|
+
}
|
|
166
|
+
return a.name.localeCompare(b.name);
|
|
167
|
+
});
|
|
168
|
+
return workspaces;
|
|
169
|
+
}
|
|
49
170
|
function extractPortFromScript(script) {
|
|
50
171
|
const portPatterns = [
|
|
51
172
|
/--port[=\s]+(\d+)/,
|
|
@@ -301,6 +422,7 @@ function detectProjectType(projectPath) {
|
|
|
301
422
|
isMonorepo: false,
|
|
302
423
|
availableScripts: [],
|
|
303
424
|
suggestedScripts: [],
|
|
425
|
+
workspaces: [],
|
|
304
426
|
compatibility: defaultCompatibility,
|
|
305
427
|
sizing
|
|
306
428
|
};
|
|
@@ -339,6 +461,7 @@ function detectProjectType(projectPath) {
|
|
|
339
461
|
type = "node";
|
|
340
462
|
defaultPort = 3e3;
|
|
341
463
|
}
|
|
464
|
+
const workspaces = isRepo ? discoverMonorepoWorkspaces(projectPath, packageJson) : [];
|
|
342
465
|
if (bestScript) {
|
|
343
466
|
return {
|
|
344
467
|
type,
|
|
@@ -349,6 +472,7 @@ function detectProjectType(projectPath) {
|
|
|
349
472
|
isMonorepo: isRepo,
|
|
350
473
|
availableScripts,
|
|
351
474
|
suggestedScripts,
|
|
475
|
+
workspaces,
|
|
352
476
|
compatibility,
|
|
353
477
|
sizing
|
|
354
478
|
};
|
|
@@ -363,6 +487,7 @@ function detectProjectType(projectPath) {
|
|
|
363
487
|
isMonorepo: isRepo,
|
|
364
488
|
availableScripts,
|
|
365
489
|
suggestedScripts,
|
|
490
|
+
workspaces,
|
|
366
491
|
compatibility,
|
|
367
492
|
sizing
|
|
368
493
|
};
|
|
@@ -419,8 +544,8 @@ function readProjectFiles(projectPath, relativePath = "", includeEnvFiles = fals
|
|
|
419
544
|
}
|
|
420
545
|
return files;
|
|
421
546
|
}
|
|
422
|
-
var MAX_CHUNK_SIZE =
|
|
423
|
-
var CHUNK_THRESHOLD =
|
|
547
|
+
var MAX_CHUNK_SIZE = 2 * 1024 * 1024;
|
|
548
|
+
var CHUNK_THRESHOLD = 3 * 1024 * 1024;
|
|
424
549
|
function calculateTotalSize(files) {
|
|
425
550
|
return Object.values(files).reduce((sum, content) => sum + content.length, 0);
|
|
426
551
|
}
|
|
@@ -736,39 +861,59 @@ async function consumeSSEStream(response, onStatus, onComplete, onError) {
|
|
|
736
861
|
}
|
|
737
862
|
const decoder = new TextDecoder();
|
|
738
863
|
let buffer = "";
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
864
|
+
let receivedAnyData = false;
|
|
865
|
+
let lastStatusMessage = "";
|
|
866
|
+
try {
|
|
867
|
+
while (true) {
|
|
868
|
+
const { done, value } = await reader.read();
|
|
869
|
+
if (done) break;
|
|
870
|
+
receivedAnyData = true;
|
|
871
|
+
buffer += decoder.decode(value, { stream: true });
|
|
872
|
+
const lines = buffer.split("\n");
|
|
873
|
+
buffer = lines.pop() || "";
|
|
874
|
+
let currentEvent = "";
|
|
875
|
+
let currentData = "";
|
|
876
|
+
for (const line of lines) {
|
|
877
|
+
if (line.startsWith("event: ")) {
|
|
878
|
+
currentEvent = line.slice(7).trim();
|
|
879
|
+
} else if (line.startsWith("data: ")) {
|
|
880
|
+
currentData = line.slice(6);
|
|
881
|
+
} else if (line === "" && currentEvent && currentData) {
|
|
882
|
+
try {
|
|
883
|
+
const data = JSON.parse(currentData);
|
|
884
|
+
switch (currentEvent) {
|
|
885
|
+
case "status":
|
|
886
|
+
lastStatusMessage = data.message;
|
|
887
|
+
onStatus(data.message);
|
|
888
|
+
break;
|
|
889
|
+
case "complete":
|
|
890
|
+
onComplete(data);
|
|
891
|
+
break;
|
|
892
|
+
case "error":
|
|
893
|
+
onError(data.message || "Server returned an error");
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
} catch (parseError) {
|
|
897
|
+
console.error("Failed to parse SSE data:", currentData, parseError);
|
|
898
|
+
onError(`Failed to parse server response: ${currentData.slice(0, 100)}`);
|
|
765
899
|
}
|
|
766
|
-
|
|
900
|
+
currentEvent = "";
|
|
901
|
+
currentData = "";
|
|
767
902
|
}
|
|
768
|
-
currentEvent = "";
|
|
769
|
-
currentData = "";
|
|
770
903
|
}
|
|
771
904
|
}
|
|
905
|
+
if (!receivedAnyData) {
|
|
906
|
+
onError("Server closed connection without sending any data");
|
|
907
|
+
}
|
|
908
|
+
} catch (streamError) {
|
|
909
|
+
const errorMsg = streamError instanceof Error ? streamError.message : String(streamError);
|
|
910
|
+
if (errorMsg.includes("aborted") || errorMsg.includes("timeout")) {
|
|
911
|
+
onError(`Request timed out. Last status: ${lastStatusMessage || "none"}`);
|
|
912
|
+
} else if (errorMsg.includes("network") || errorMsg.includes("fetch")) {
|
|
913
|
+
onError(`Network error: ${errorMsg}`);
|
|
914
|
+
} else {
|
|
915
|
+
onError(`Stream error: ${errorMsg}. Last status: ${lastStatusMessage || "none"}`);
|
|
916
|
+
}
|
|
772
917
|
}
|
|
773
918
|
}
|
|
774
919
|
async function deployProject(files, options, log) {
|
|
@@ -953,8 +1098,17 @@ async function createSandbox(options, log) {
|
|
|
953
1098
|
})
|
|
954
1099
|
});
|
|
955
1100
|
if (!response.ok) {
|
|
956
|
-
const
|
|
957
|
-
|
|
1101
|
+
const text = await response.text().catch(() => "");
|
|
1102
|
+
let errorMessage = `Failed to create sandbox (HTTP ${response.status})`;
|
|
1103
|
+
try {
|
|
1104
|
+
const data2 = JSON.parse(text);
|
|
1105
|
+
errorMessage = data2.error || data2.message || errorMessage;
|
|
1106
|
+
} catch {
|
|
1107
|
+
if (text) {
|
|
1108
|
+
errorMessage = text.slice(0, 200);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
throw new APIError(errorMessage, response.status);
|
|
958
1112
|
}
|
|
959
1113
|
const data = await response.json();
|
|
960
1114
|
if (!data.success) {
|
|
@@ -983,8 +1137,17 @@ async function uploadChunk(sandboxId, files, chunkIndex, totalChunks, log) {
|
|
|
983
1137
|
})
|
|
984
1138
|
});
|
|
985
1139
|
if (!response.ok) {
|
|
986
|
-
const
|
|
987
|
-
|
|
1140
|
+
const text = await response.text().catch(() => "");
|
|
1141
|
+
let errorMessage = `Failed to upload chunk ${chunkIndex} (HTTP ${response.status})`;
|
|
1142
|
+
try {
|
|
1143
|
+
const data2 = JSON.parse(text);
|
|
1144
|
+
errorMessage = data2.error || data2.message || errorMessage;
|
|
1145
|
+
} catch {
|
|
1146
|
+
if (text) {
|
|
1147
|
+
errorMessage = text.slice(0, 200);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
throw new APIError(errorMessage, response.status);
|
|
988
1151
|
}
|
|
989
1152
|
const data = await response.json();
|
|
990
1153
|
if (!data.success) {
|
|
@@ -1160,15 +1323,504 @@ function autoCommitForShare(projectPath, inflightVersionId) {
|
|
|
1160
1323
|
}
|
|
1161
1324
|
}
|
|
1162
1325
|
|
|
1326
|
+
// src/utils/patch-embed.ts
|
|
1327
|
+
function alreadyAllowsInflight(content) {
|
|
1328
|
+
return content.includes("inflight.co") && (content.includes("frame-ancestors") || content.includes("X-Frame-Options"));
|
|
1329
|
+
}
|
|
1330
|
+
function checkNextConfig(content, filePath) {
|
|
1331
|
+
if (alreadyAllowsInflight(content)) {
|
|
1332
|
+
return { canEmbed: true, issues: [], configFile: filePath };
|
|
1333
|
+
}
|
|
1334
|
+
const issues = [];
|
|
1335
|
+
if (content.includes("X-Frame-Options")) {
|
|
1336
|
+
const xfoMatch = content.match(/X-Frame-Options['":\s]+['"]?(DENY|SAMEORIGIN)/i);
|
|
1337
|
+
if (xfoMatch) {
|
|
1338
|
+
issues.push(`X-Frame-Options is set to ${xfoMatch[1]} which blocks embedding`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (content.includes("frame-ancestors")) {
|
|
1342
|
+
const cspMatch = content.match(/frame-ancestors['":\s]+['"]?([^'"}\n]+)/i);
|
|
1343
|
+
if (cspMatch) {
|
|
1344
|
+
const value = cspMatch[1].toLowerCase();
|
|
1345
|
+
if ((value.includes("'none'") || value.includes("'self'")) && !value.includes("inflight")) {
|
|
1346
|
+
issues.push(`CSP frame-ancestors is restrictive: ${cspMatch[1].trim()}`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (content.includes("async headers()") || content.includes("headers:")) {
|
|
1351
|
+
if (issues.length === 0) {
|
|
1352
|
+
issues.push("Has custom headers configuration - may need to add inflight.co to frame-ancestors");
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
canEmbed: issues.length === 0,
|
|
1357
|
+
issues,
|
|
1358
|
+
configFile: filePath
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function checkMiddleware(content, filePath) {
|
|
1362
|
+
if (alreadyAllowsInflight(content)) {
|
|
1363
|
+
return { canEmbed: true, issues: [], configFile: filePath };
|
|
1364
|
+
}
|
|
1365
|
+
const issues = [];
|
|
1366
|
+
if (content.includes("X-Frame-Options")) {
|
|
1367
|
+
const xfoMatch = content.match(/['"](X-Frame-Options)['"]\s*,\s*['"]?(DENY|SAMEORIGIN)['"]?/i);
|
|
1368
|
+
if (xfoMatch) {
|
|
1369
|
+
issues.push(`Middleware sets X-Frame-Options to ${xfoMatch[2]} which blocks embedding`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (content.includes("frame-ancestors")) {
|
|
1373
|
+
const cspMatch = content.match(/frame-ancestors\s+([^'"}\n;]+)/i);
|
|
1374
|
+
if (cspMatch) {
|
|
1375
|
+
const value = cspMatch[1].toLowerCase();
|
|
1376
|
+
if ((value.includes("'none'") || value.includes("'self'")) && !value.includes("inflight")) {
|
|
1377
|
+
issues.push(`CSP frame-ancestors is restrictive: ${cspMatch[1].trim()}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
canEmbed: issues.length === 0,
|
|
1383
|
+
issues,
|
|
1384
|
+
configFile: filePath
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function checkViteConfig(content, filePath) {
|
|
1388
|
+
if (content.includes(".csb.app") || content.includes(".codesandbox.io")) {
|
|
1389
|
+
return { canEmbed: true, issues: [], configFile: filePath };
|
|
1390
|
+
}
|
|
1391
|
+
const issues = [];
|
|
1392
|
+
if (content.includes("allowedHosts")) {
|
|
1393
|
+
const match = content.match(/allowedHosts\s*:\s*(?:'([^']*)'|"([^"]*)"|(\[[^\]]*\]))/);
|
|
1394
|
+
if (match) {
|
|
1395
|
+
const value = match[1] || match[2] || match[3] || "";
|
|
1396
|
+
if (!value.includes(".csb.app") && !value.includes("codesandbox") && value !== "true") {
|
|
1397
|
+
issues.push(`allowedHosts is restrictive - needs .csb.app and .codesandbox.io`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
canEmbed: issues.length === 0,
|
|
1403
|
+
issues,
|
|
1404
|
+
configFile: filePath
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
function checkHeadersFile(content, filePath) {
|
|
1408
|
+
if (alreadyAllowsInflight(content)) {
|
|
1409
|
+
return { canEmbed: true, issues: [], configFile: filePath };
|
|
1410
|
+
}
|
|
1411
|
+
const issues = [];
|
|
1412
|
+
if (content.includes("X-Frame-Options")) {
|
|
1413
|
+
const xfoMatch = content.match(/X-Frame-Options:\s*(DENY|SAMEORIGIN)/i);
|
|
1414
|
+
if (xfoMatch) {
|
|
1415
|
+
issues.push(`_headers file sets X-Frame-Options: ${xfoMatch[1]} which blocks embedding`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (content.includes("frame-ancestors")) {
|
|
1419
|
+
const cspMatch = content.match(/frame-ancestors\s+([^\n;]+)/i);
|
|
1420
|
+
if (cspMatch) {
|
|
1421
|
+
const value = cspMatch[1].toLowerCase();
|
|
1422
|
+
if ((value.includes("'none'") || value.includes("'self'")) && !value.includes("inflight")) {
|
|
1423
|
+
issues.push(`_headers file has restrictive frame-ancestors: ${cspMatch[1].trim()}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
canEmbed: issues.length === 0,
|
|
1429
|
+
issues,
|
|
1430
|
+
configFile: filePath
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function checkVercelJson(content, filePath) {
|
|
1434
|
+
if (alreadyAllowsInflight(content)) {
|
|
1435
|
+
return { canEmbed: true, issues: [], configFile: filePath };
|
|
1436
|
+
}
|
|
1437
|
+
const issues = [];
|
|
1438
|
+
try {
|
|
1439
|
+
const config = JSON.parse(content);
|
|
1440
|
+
if (config.headers) {
|
|
1441
|
+
for (const headerConfig of config.headers) {
|
|
1442
|
+
for (const header of headerConfig.headers || []) {
|
|
1443
|
+
if (header.key?.toLowerCase() === "x-frame-options") {
|
|
1444
|
+
if (header.value?.toUpperCase() === "DENY" || header.value?.toUpperCase() === "SAMEORIGIN") {
|
|
1445
|
+
issues.push(`vercel.json sets X-Frame-Options: ${header.value} which blocks embedding`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
if (header.key?.toLowerCase() === "content-security-policy" && header.value?.includes("frame-ancestors")) {
|
|
1449
|
+
const value = header.value.toLowerCase();
|
|
1450
|
+
if ((value.includes("'none'") || value.includes("'self'")) && !value.includes("inflight")) {
|
|
1451
|
+
issues.push(`vercel.json has restrictive CSP frame-ancestors`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
canEmbed: issues.length === 0,
|
|
1461
|
+
issues,
|
|
1462
|
+
configFile: filePath
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function isMiddlewareFile(filePath) {
|
|
1466
|
+
const fileName = filePath.split("/").pop() || "";
|
|
1467
|
+
if (fileName === "middleware.ts" || fileName === "middleware.js") {
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
if (filePath.includes("/middleware") && (fileName.endsWith(".ts") || fileName.endsWith(".js"))) {
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
var INFLIGHT_FRAME_ANCESTORS = "frame-ancestors 'self' https://*.inflight.co https://inflight.co";
|
|
1476
|
+
function patchSupabaseCookies(content) {
|
|
1477
|
+
if (!content.includes("createServerClient") || !content.includes("setAll")) {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
if (content.includes("sameSite") && content.includes("none")) {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
const setAllPattern = /(cookiesToSet\.forEach\s*\(\s*\(\s*\{\s*name\s*,\s*value\s*,\s*options\s*\}\s*\)\s*=>\s*)([\w.]+\.cookies\.set\s*\(\s*name\s*,\s*value\s*,\s*options\s*\))/g;
|
|
1484
|
+
if (!setAllPattern.test(content)) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
setAllPattern.lastIndex = 0;
|
|
1488
|
+
const patched = content.replace(setAllPattern, (_match, prefix, setCookieCall) => {
|
|
1489
|
+
const patchedSetCall = setCookieCall.replace(
|
|
1490
|
+
/\.cookies\.set\s*\(\s*name\s*,\s*value\s*,\s*options\s*\)/,
|
|
1491
|
+
".cookies.set(name, value, { ...options, sameSite: 'none', secure: true })"
|
|
1492
|
+
);
|
|
1493
|
+
return prefix + patchedSetCall;
|
|
1494
|
+
});
|
|
1495
|
+
return patched !== content ? patched : null;
|
|
1496
|
+
}
|
|
1497
|
+
function patchMiddleware(content) {
|
|
1498
|
+
if (alreadyAllowsInflight(content)) {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
if (!content.includes("X-Frame-Options")) {
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
const xfoRegex = /(\s*)([\w.]+\.headers\.set\s*\(\s*['"]X-Frame-Options['"]\s*,\s*['"][^'"]*['"]\s*\)\s*;?)/gi;
|
|
1505
|
+
if (!xfoRegex.test(content)) {
|
|
1506
|
+
return null;
|
|
1507
|
+
}
|
|
1508
|
+
xfoRegex.lastIndex = 0;
|
|
1509
|
+
const patched = content.replace(xfoRegex, (match, indent, xfoLine) => {
|
|
1510
|
+
const varMatch = xfoLine.match(/([\w.]+)\.headers\.set/);
|
|
1511
|
+
const varName = varMatch ? varMatch[1] : "response";
|
|
1512
|
+
const cspLine = `${indent}${varName}.headers.set("Content-Security-Policy", "${INFLIGHT_FRAME_ANCESTORS}");`;
|
|
1513
|
+
return `${match}
|
|
1514
|
+
${cspLine}`;
|
|
1515
|
+
});
|
|
1516
|
+
return patched !== content ? patched : null;
|
|
1517
|
+
}
|
|
1518
|
+
function patchNextConfig(content) {
|
|
1519
|
+
if (alreadyAllowsInflight(content)) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
if (!content.includes("X-Frame-Options") && !content.includes("frame-ancestors")) {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
function patchHeadersFile(content) {
|
|
1528
|
+
if (alreadyAllowsInflight(content)) {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
if (!content.includes("X-Frame-Options") && !content.includes("frame-ancestors")) {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
let patched = content;
|
|
1535
|
+
patched = patched.replace(/^\s*X-Frame-Options:\s*(DENY|SAMEORIGIN)\s*$/gim, "");
|
|
1536
|
+
if (patched.includes("/*")) {
|
|
1537
|
+
patched = patched.replace(/(\/\*\s*\n)/, `$1 Content-Security-Policy: ${INFLIGHT_FRAME_ANCESTORS}
|
|
1538
|
+
`);
|
|
1539
|
+
}
|
|
1540
|
+
return patched !== content ? patched : null;
|
|
1541
|
+
}
|
|
1542
|
+
function patchVercelJson(content) {
|
|
1543
|
+
if (alreadyAllowsInflight(content)) {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
const config = JSON.parse(content);
|
|
1548
|
+
let modified = false;
|
|
1549
|
+
if (config.headers) {
|
|
1550
|
+
for (const headerConfig of config.headers) {
|
|
1551
|
+
if (headerConfig.headers) {
|
|
1552
|
+
headerConfig.headers = headerConfig.headers.filter((h) => {
|
|
1553
|
+
if (h.key?.toLowerCase() === "x-frame-options") {
|
|
1554
|
+
if (h.value?.toUpperCase() === "DENY" || h.value?.toUpperCase() === "SAMEORIGIN") {
|
|
1555
|
+
modified = true;
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return true;
|
|
1560
|
+
});
|
|
1561
|
+
const hasCSP = headerConfig.headers.some(
|
|
1562
|
+
(h) => h.key?.toLowerCase() === "content-security-policy"
|
|
1563
|
+
);
|
|
1564
|
+
if (!hasCSP && modified) {
|
|
1565
|
+
headerConfig.headers.push({
|
|
1566
|
+
key: "Content-Security-Policy",
|
|
1567
|
+
value: INFLIGHT_FRAME_ANCESTORS
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return modified ? JSON.stringify(config, null, 2) : null;
|
|
1574
|
+
} catch {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function patchFilesForEmbed(files) {
|
|
1579
|
+
const patchedFiles = [];
|
|
1580
|
+
const remainingIssues = [];
|
|
1581
|
+
const result = { ...files };
|
|
1582
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
1583
|
+
const fileName = filePath.split("/").pop() || "";
|
|
1584
|
+
let patched = null;
|
|
1585
|
+
if (isMiddlewareFile(filePath)) {
|
|
1586
|
+
let middlewareContent = content;
|
|
1587
|
+
let middlewarePatched = false;
|
|
1588
|
+
const xfoPatch = patchMiddleware(middlewareContent);
|
|
1589
|
+
if (xfoPatch) {
|
|
1590
|
+
middlewareContent = xfoPatch;
|
|
1591
|
+
middlewarePatched = true;
|
|
1592
|
+
} else if (middlewareContent.includes("X-Frame-Options") && !alreadyAllowsInflight(middlewareContent)) {
|
|
1593
|
+
remainingIssues.push(`${filePath}: Could not auto-patch X-Frame-Options`);
|
|
1594
|
+
}
|
|
1595
|
+
const cookiePatch = patchSupabaseCookies(middlewareContent);
|
|
1596
|
+
if (cookiePatch) {
|
|
1597
|
+
middlewareContent = cookiePatch;
|
|
1598
|
+
middlewarePatched = true;
|
|
1599
|
+
}
|
|
1600
|
+
if (middlewarePatched) {
|
|
1601
|
+
result[filePath] = middlewareContent;
|
|
1602
|
+
patchedFiles.push(filePath);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (fileName === "next.config.js" || fileName === "next.config.mjs" || fileName === "next.config.ts") {
|
|
1606
|
+
patched = patchNextConfig(content);
|
|
1607
|
+
if (patched) {
|
|
1608
|
+
result[filePath] = patched;
|
|
1609
|
+
patchedFiles.push(filePath);
|
|
1610
|
+
} else if ((content.includes("X-Frame-Options") || content.includes("frame-ancestors")) && !alreadyAllowsInflight(content)) {
|
|
1611
|
+
remainingIssues.push(`${filePath}: Has custom headers - may need manual review`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (fileName === "_headers") {
|
|
1615
|
+
patched = patchHeadersFile(content);
|
|
1616
|
+
if (patched) {
|
|
1617
|
+
result[filePath] = patched;
|
|
1618
|
+
patchedFiles.push(filePath);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (fileName === "vercel.json") {
|
|
1622
|
+
patched = patchVercelJson(content);
|
|
1623
|
+
if (patched) {
|
|
1624
|
+
result[filePath] = patched;
|
|
1625
|
+
patchedFiles.push(filePath);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (filePath.includes("supabase") && fileName === "server.ts") {
|
|
1629
|
+
patched = patchSupabaseCookies(content);
|
|
1630
|
+
if (patched) {
|
|
1631
|
+
result[filePath] = patched;
|
|
1632
|
+
patchedFiles.push(filePath);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
files: result,
|
|
1638
|
+
patchedFiles,
|
|
1639
|
+
remainingIssues
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function checkEmbedCompatibility(files) {
|
|
1643
|
+
const allIssues = [];
|
|
1644
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
1645
|
+
const fileName = filePath.split("/").pop() || "";
|
|
1646
|
+
if (fileName === "next.config.js" || fileName === "next.config.mjs" || fileName === "next.config.ts") {
|
|
1647
|
+
const result = checkNextConfig(content, filePath);
|
|
1648
|
+
if (!result.canEmbed) {
|
|
1649
|
+
allIssues.push({ file: filePath, issues: result.issues });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (isMiddlewareFile(filePath)) {
|
|
1653
|
+
const result = checkMiddleware(content, filePath);
|
|
1654
|
+
if (!result.canEmbed) {
|
|
1655
|
+
allIssues.push({ file: filePath, issues: result.issues });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (fileName === "vite.config.js" || fileName === "vite.config.ts") {
|
|
1659
|
+
const result = checkViteConfig(content, filePath);
|
|
1660
|
+
if (!result.canEmbed) {
|
|
1661
|
+
allIssues.push({ file: filePath, issues: result.issues });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (fileName === "_headers") {
|
|
1665
|
+
const result = checkHeadersFile(content, filePath);
|
|
1666
|
+
if (!result.canEmbed) {
|
|
1667
|
+
allIssues.push({ file: filePath, issues: result.issues });
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (fileName === "vercel.json") {
|
|
1671
|
+
const result = checkVercelJson(content, filePath);
|
|
1672
|
+
if (!result.canEmbed) {
|
|
1673
|
+
allIssues.push({ file: filePath, issues: result.issues });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (allIssues.length > 0) {
|
|
1678
|
+
return {
|
|
1679
|
+
canEmbed: false,
|
|
1680
|
+
issues: allIssues[0].issues,
|
|
1681
|
+
configFile: allIssues[0].file
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
return { canEmbed: true, issues: [], configFile: null };
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/utils/workspace-bundle.ts
|
|
1688
|
+
import * as fs4 from "fs";
|
|
1689
|
+
import * as path4 from "path";
|
|
1690
|
+
function findMonorepoRoot(startPath) {
|
|
1691
|
+
let current = startPath;
|
|
1692
|
+
const root = path4.parse(current).root;
|
|
1693
|
+
while (current !== root) {
|
|
1694
|
+
if (fs4.existsSync(path4.join(current, "pnpm-workspace.yaml"))) {
|
|
1695
|
+
return current;
|
|
1696
|
+
}
|
|
1697
|
+
const pkgPath = path4.join(current, "package.json");
|
|
1698
|
+
if (fs4.existsSync(pkgPath)) {
|
|
1699
|
+
try {
|
|
1700
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1701
|
+
if (pkg.workspaces) {
|
|
1702
|
+
return current;
|
|
1703
|
+
}
|
|
1704
|
+
} catch {
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
current = path4.dirname(current);
|
|
1708
|
+
}
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
function hasWorkspaceDependencies(pkgContent) {
|
|
1712
|
+
try {
|
|
1713
|
+
const pkg = JSON.parse(pkgContent);
|
|
1714
|
+
const allDeps = {
|
|
1715
|
+
...pkg.dependencies,
|
|
1716
|
+
...pkg.devDependencies
|
|
1717
|
+
};
|
|
1718
|
+
for (const version of Object.values(allDeps)) {
|
|
1719
|
+
if (version.startsWith("workspace:")) {
|
|
1720
|
+
return true;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
} catch {
|
|
1724
|
+
}
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
function detectMonorepoDeployment(projectPath, packageManager, originalStartCommand) {
|
|
1728
|
+
const pkgPath = path4.join(projectPath, "package.json");
|
|
1729
|
+
if (!fs4.existsSync(pkgPath)) {
|
|
1730
|
+
return {
|
|
1731
|
+
rootPath: projectPath,
|
|
1732
|
+
appPath: null,
|
|
1733
|
+
isMonorepo: false,
|
|
1734
|
+
startCommand: null,
|
|
1735
|
+
installCommand: null
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
const pkgContent = fs4.readFileSync(pkgPath, "utf-8");
|
|
1739
|
+
if (!hasWorkspaceDependencies(pkgContent)) {
|
|
1740
|
+
return {
|
|
1741
|
+
rootPath: projectPath,
|
|
1742
|
+
appPath: null,
|
|
1743
|
+
isMonorepo: false,
|
|
1744
|
+
startCommand: null,
|
|
1745
|
+
installCommand: null
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
const monorepoRoot = findMonorepoRoot(projectPath);
|
|
1749
|
+
if (!monorepoRoot || monorepoRoot === projectPath) {
|
|
1750
|
+
return {
|
|
1751
|
+
rootPath: projectPath,
|
|
1752
|
+
appPath: null,
|
|
1753
|
+
isMonorepo: false,
|
|
1754
|
+
startCommand: null,
|
|
1755
|
+
installCommand: null
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
const appPath = path4.relative(monorepoRoot, projectPath);
|
|
1759
|
+
let packageName = null;
|
|
1760
|
+
try {
|
|
1761
|
+
const pkg = JSON.parse(pkgContent);
|
|
1762
|
+
packageName = pkg.name || null;
|
|
1763
|
+
} catch {
|
|
1764
|
+
}
|
|
1765
|
+
let startCommand;
|
|
1766
|
+
let installCommand;
|
|
1767
|
+
if (packageManager === "pnpm") {
|
|
1768
|
+
const scriptMatch = originalStartCommand.match(/(?:pnpm|npm run|yarn)\s+(\S+)/);
|
|
1769
|
+
const scriptName = scriptMatch ? scriptMatch[1] : "dev";
|
|
1770
|
+
if (packageName) {
|
|
1771
|
+
startCommand = `pnpm --filter "${packageName}" ${scriptName}`;
|
|
1772
|
+
} else {
|
|
1773
|
+
startCommand = `pnpm --filter "./${appPath}" ${scriptName}`;
|
|
1774
|
+
}
|
|
1775
|
+
installCommand = "pnpm install";
|
|
1776
|
+
} else if (packageManager === "yarn") {
|
|
1777
|
+
const scriptMatch = originalStartCommand.match(/(?:yarn|npm run)\s+(\S+)/);
|
|
1778
|
+
const scriptName = scriptMatch ? scriptMatch[1] : "dev";
|
|
1779
|
+
if (packageName) {
|
|
1780
|
+
startCommand = `yarn workspace ${packageName} ${scriptName}`;
|
|
1781
|
+
} else {
|
|
1782
|
+
startCommand = `cd ${appPath} && yarn ${scriptName}`;
|
|
1783
|
+
}
|
|
1784
|
+
installCommand = "yarn install";
|
|
1785
|
+
} else {
|
|
1786
|
+
const scriptMatch = originalStartCommand.match(/(?:npm run|npm)\s+(\S+)/);
|
|
1787
|
+
const scriptName = scriptMatch ? scriptMatch[1] : "dev";
|
|
1788
|
+
if (packageName) {
|
|
1789
|
+
startCommand = `npm run ${scriptName} --workspace=${packageName}`;
|
|
1790
|
+
} else {
|
|
1791
|
+
startCommand = `cd ${appPath} && npm run ${scriptName}`;
|
|
1792
|
+
}
|
|
1793
|
+
installCommand = "npm install";
|
|
1794
|
+
}
|
|
1795
|
+
return {
|
|
1796
|
+
rootPath: monorepoRoot,
|
|
1797
|
+
appPath,
|
|
1798
|
+
isMonorepo: true,
|
|
1799
|
+
startCommand,
|
|
1800
|
+
installCommand
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function readMonorepoFiles(projectPath, packageManager, originalStartCommand, includeEnvFiles = false) {
|
|
1804
|
+
const monorepoResult = detectMonorepoDeployment(projectPath, packageManager, originalStartCommand);
|
|
1805
|
+
const files = readProjectFiles(monorepoResult.rootPath, "", includeEnvFiles);
|
|
1806
|
+
return {
|
|
1807
|
+
files,
|
|
1808
|
+
rootPath: monorepoResult.rootPath,
|
|
1809
|
+
isMonorepo: monorepoResult.isMonorepo,
|
|
1810
|
+
startCommand: monorepoResult.startCommand,
|
|
1811
|
+
installCommand: monorepoResult.installCommand
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1163
1815
|
// src/tools/deploy.ts
|
|
1164
1816
|
async function deployPrototype(args, log) {
|
|
1165
1817
|
const startTime = Date.now();
|
|
1166
1818
|
await log(`Starting share for: ${args.path}`);
|
|
1167
|
-
const projectPath =
|
|
1168
|
-
if (!
|
|
1819
|
+
const projectPath = path5.resolve(args.path);
|
|
1820
|
+
if (!fs5.existsSync(projectPath)) {
|
|
1169
1821
|
throw new Error(`Project path does not exist: ${projectPath}`);
|
|
1170
1822
|
}
|
|
1171
|
-
if (!
|
|
1823
|
+
if (!fs5.statSync(projectPath).isDirectory()) {
|
|
1172
1824
|
throw new Error(`Path is not a directory: ${projectPath}`);
|
|
1173
1825
|
}
|
|
1174
1826
|
if (!isAuthenticated()) {
|
|
@@ -1217,17 +1869,55 @@ async function deployPrototype(args, log) {
|
|
|
1217
1869
|
if (includeEnv) {
|
|
1218
1870
|
await log("Including .env files as requested");
|
|
1219
1871
|
}
|
|
1220
|
-
const
|
|
1872
|
+
const monorepoResult = readMonorepoFiles(
|
|
1873
|
+
projectPath,
|
|
1874
|
+
projectInfo.detectedPackageManager,
|
|
1875
|
+
projectInfo.startCommand,
|
|
1876
|
+
includeEnv
|
|
1877
|
+
);
|
|
1878
|
+
let files = monorepoResult.files;
|
|
1221
1879
|
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
1880
|
if (fileCount === 0) {
|
|
1225
1881
|
throw new Error("No files found in project directory");
|
|
1226
1882
|
}
|
|
1883
|
+
if (monorepoResult.isMonorepo) {
|
|
1884
|
+
await log(`Monorepo detected - including full workspace from ${path5.basename(monorepoResult.rootPath)}`);
|
|
1885
|
+
if (monorepoResult.startCommand) {
|
|
1886
|
+
projectInfo.startCommand = monorepoResult.startCommand;
|
|
1887
|
+
await log(`Adjusted start command: ${projectInfo.startCommand}`);
|
|
1888
|
+
}
|
|
1889
|
+
if (monorepoResult.installCommand) {
|
|
1890
|
+
projectInfo.installCommand = monorepoResult.installCommand;
|
|
1891
|
+
await log(`Adjusted install command: ${projectInfo.installCommand}`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
let embedCheck = checkEmbedCompatibility(files);
|
|
1895
|
+
if (!embedCheck.canEmbed) {
|
|
1896
|
+
await log(`Embedding issues detected in ${embedCheck.configFile}:`, "warning");
|
|
1897
|
+
for (const issue of embedCheck.issues) {
|
|
1898
|
+
await log(` - ${issue}`, "warning");
|
|
1899
|
+
}
|
|
1900
|
+
await log("Patching files to allow InFlight embedding...");
|
|
1901
|
+
const patchResult = patchFilesForEmbed(files);
|
|
1902
|
+
if (patchResult.patchedFiles.length > 0) {
|
|
1903
|
+
files = patchResult.files;
|
|
1904
|
+
for (const patchedFile of patchResult.patchedFiles) {
|
|
1905
|
+
await log(` \u2713 Patched: ${patchedFile}`);
|
|
1906
|
+
}
|
|
1907
|
+
embedCheck = checkEmbedCompatibility(files);
|
|
1908
|
+
}
|
|
1909
|
+
if (patchResult.remainingIssues.length > 0) {
|
|
1910
|
+
for (const issue of patchResult.remainingIssues) {
|
|
1911
|
+
await log(` \u26A0\uFE0F ${issue}`, "warning");
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
const totalSize = Object.values(files).reduce((sum, content) => sum + content.length, 0);
|
|
1916
|
+
await log(`Found ${fileCount} files (${(totalSize / 1024 / 1024).toFixed(1)} MB)`);
|
|
1227
1917
|
let existingSandbox;
|
|
1228
1918
|
try {
|
|
1229
1919
|
const sandboxes = await listSandboxes();
|
|
1230
|
-
const projectName =
|
|
1920
|
+
const projectName = path5.basename(projectPath);
|
|
1231
1921
|
existingSandbox = sandboxes.find(
|
|
1232
1922
|
(s) => s.projectName === projectName && s.status === "running"
|
|
1233
1923
|
);
|
|
@@ -1257,7 +1947,13 @@ async function deployPrototype(args, log) {
|
|
|
1257
1947
|
projectType: projectInfo.type,
|
|
1258
1948
|
synced: true,
|
|
1259
1949
|
inflightUrl,
|
|
1260
|
-
inflightVersionId
|
|
1950
|
+
inflightVersionId,
|
|
1951
|
+
startCommand: projectInfo.startCommand,
|
|
1952
|
+
port: projectInfo.port,
|
|
1953
|
+
packageManager: projectInfo.detectedPackageManager,
|
|
1954
|
+
isMonorepo: projectInfo.isMonorepo,
|
|
1955
|
+
vmTier: projectInfo.sizing.recommendedTier,
|
|
1956
|
+
embedCheck
|
|
1261
1957
|
};
|
|
1262
1958
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
1263
1959
|
await log(`Sync complete in ${elapsed}s`);
|
|
@@ -1282,7 +1978,7 @@ async function deployPrototype(args, log) {
|
|
|
1282
1978
|
startCommand: projectInfo.startCommand,
|
|
1283
1979
|
port: projectInfo.port,
|
|
1284
1980
|
vmTier: projectInfo.sizing.recommendedTier,
|
|
1285
|
-
projectName:
|
|
1981
|
+
projectName: path5.basename(projectPath),
|
|
1286
1982
|
workspaceId: args.workspaceId,
|
|
1287
1983
|
gitInfo: gitInfo || void 0
|
|
1288
1984
|
},
|
|
@@ -1294,7 +1990,13 @@ async function deployPrototype(args, log) {
|
|
|
1294
1990
|
projectType: projectInfo.type,
|
|
1295
1991
|
synced: false,
|
|
1296
1992
|
inflightUrl: deployResult.inflightUrl,
|
|
1297
|
-
inflightVersionId: deployResult.versionId
|
|
1993
|
+
inflightVersionId: deployResult.versionId,
|
|
1994
|
+
startCommand: projectInfo.startCommand,
|
|
1995
|
+
port: projectInfo.port,
|
|
1996
|
+
packageManager: projectInfo.detectedPackageManager,
|
|
1997
|
+
isMonorepo: projectInfo.isMonorepo,
|
|
1998
|
+
vmTier: projectInfo.sizing.recommendedTier,
|
|
1999
|
+
embedCheck
|
|
1298
2000
|
};
|
|
1299
2001
|
} else {
|
|
1300
2002
|
await log("Deploying via InFlight API...");
|
|
@@ -1306,7 +2008,7 @@ async function deployPrototype(args, log) {
|
|
|
1306
2008
|
startCommand: projectInfo.startCommand,
|
|
1307
2009
|
port: projectInfo.port,
|
|
1308
2010
|
vmTier: projectInfo.sizing.recommendedTier,
|
|
1309
|
-
projectName:
|
|
2011
|
+
projectName: path5.basename(projectPath),
|
|
1310
2012
|
workspaceId: args.workspaceId,
|
|
1311
2013
|
gitInfo: gitInfo || void 0
|
|
1312
2014
|
},
|
|
@@ -1318,7 +2020,13 @@ async function deployPrototype(args, log) {
|
|
|
1318
2020
|
projectType: projectInfo.type,
|
|
1319
2021
|
synced: false,
|
|
1320
2022
|
inflightUrl: deployResult.inflightUrl,
|
|
1321
|
-
inflightVersionId: deployResult.versionId
|
|
2023
|
+
inflightVersionId: deployResult.versionId,
|
|
2024
|
+
startCommand: projectInfo.startCommand,
|
|
2025
|
+
port: projectInfo.port,
|
|
2026
|
+
packageManager: projectInfo.detectedPackageManager,
|
|
2027
|
+
isMonorepo: projectInfo.isMonorepo,
|
|
2028
|
+
vmTier: projectInfo.sizing.recommendedTier,
|
|
2029
|
+
embedCheck
|
|
1322
2030
|
};
|
|
1323
2031
|
}
|
|
1324
2032
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
@@ -1340,13 +2048,13 @@ async function deployPrototype(args, log) {
|
|
|
1340
2048
|
}
|
|
1341
2049
|
|
|
1342
2050
|
// src/server.ts
|
|
1343
|
-
import * as
|
|
1344
|
-
import * as
|
|
2051
|
+
import * as path6 from "path";
|
|
2052
|
+
import * as fs6 from "fs";
|
|
1345
2053
|
function createServer2() {
|
|
1346
2054
|
const server = new Server(
|
|
1347
2055
|
{
|
|
1348
2056
|
name: "mcp-inflight",
|
|
1349
|
-
version: "0.2.
|
|
2057
|
+
version: "0.2.2"
|
|
1350
2058
|
},
|
|
1351
2059
|
{
|
|
1352
2060
|
capabilities: {
|
|
@@ -1370,21 +2078,21 @@ function createServer2() {
|
|
|
1370
2078
|
tools: [
|
|
1371
2079
|
{
|
|
1372
2080
|
name: "share",
|
|
1373
|
-
description: "Share a local project by deploying it to a public URL and creating an InFlight version for feedback.
|
|
2081
|
+
description: "Share a local project by deploying it to a public URL and creating an InFlight version for feedback. IMPORTANT: Before calling this tool, you MUST check for iframe embedding blockers: 1. Search for middleware.ts files and check for X-Frame-Options headers (DENY or SAMEORIGIN block embedding). 2. Check next.config.js/ts for headers() that set X-Frame-Options or restrictive frame-ancestors. 3. If blocking headers are found, ask the user if you should add a CSP frame-ancestors header to allow inflight.co to embed the prototype. The fix is to add: Content-Security-Policy: frame-ancestors 'self' https://*.inflight.co https://inflight.co 4. For monorepos, determine which app/package to deploy and provide the specific path. 5. Check package.json scripts to find the correct dev command and port. Common patterns: Next.js (port 3000), Vite (port 5173), CRA (port 3000). Returns detected configuration, embed check results, and InFlight URL. Requires InFlight authentication.",
|
|
1374
2082
|
inputSchema: {
|
|
1375
2083
|
type: "object",
|
|
1376
2084
|
properties: {
|
|
1377
2085
|
path: {
|
|
1378
2086
|
type: "string",
|
|
1379
|
-
description: "Absolute path to the project directory to share"
|
|
2087
|
+
description: "Absolute path to the project directory to share. For monorepos, provide the path to the specific app/package (e.g., '/path/to/monorepo/apps/web')."
|
|
1380
2088
|
},
|
|
1381
2089
|
port: {
|
|
1382
2090
|
type: "number",
|
|
1383
|
-
description: "Port the dev server runs on
|
|
2091
|
+
description: "Port the dev server runs on. Auto-detected from package.json scripts, but provide explicitly if known (e.g., from 'next dev -p 3001')."
|
|
1384
2092
|
},
|
|
1385
2093
|
command: {
|
|
1386
2094
|
type: "string",
|
|
1387
|
-
description: "
|
|
2095
|
+
description: "Start command for the dev server. Auto-detected but provide explicitly for custom setups. Examples: 'npm run dev', 'pnpm dev', 'node server.js'."
|
|
1388
2096
|
},
|
|
1389
2097
|
includeEnvFiles: {
|
|
1390
2098
|
type: "boolean",
|
|
@@ -1479,7 +2187,17 @@ function createServer2() {
|
|
|
1479
2187
|
text: JSON.stringify(
|
|
1480
2188
|
{
|
|
1481
2189
|
success: true,
|
|
1482
|
-
|
|
2190
|
+
// Detected configuration - communicate this to the user
|
|
2191
|
+
detectedConfig: {
|
|
2192
|
+
projectType: result.projectType,
|
|
2193
|
+
startCommand: result.startCommand,
|
|
2194
|
+
port: result.port,
|
|
2195
|
+
packageManager: result.packageManager,
|
|
2196
|
+
isMonorepo: result.isMonorepo,
|
|
2197
|
+
vmTier: result.vmTier
|
|
2198
|
+
},
|
|
2199
|
+
// Embed check results - shows if there are iframe embedding issues
|
|
2200
|
+
embedCheck: result.embedCheck,
|
|
1483
2201
|
synced: result.synced ?? false,
|
|
1484
2202
|
inflightUrl: result.inflightUrl,
|
|
1485
2203
|
inflightVersionId: result.inflightVersionId,
|
|
@@ -1498,8 +2216,8 @@ function createServer2() {
|
|
|
1498
2216
|
await log("Authenticating with InFlight...");
|
|
1499
2217
|
await authenticate(log);
|
|
1500
2218
|
}
|
|
1501
|
-
const resolvedPath =
|
|
1502
|
-
if (!
|
|
2219
|
+
const resolvedPath = path6.resolve(projectPath);
|
|
2220
|
+
if (!fs6.existsSync(resolvedPath)) {
|
|
1503
2221
|
throw new Error(`Project path does not exist: ${resolvedPath}`);
|
|
1504
2222
|
}
|
|
1505
2223
|
const files = readProjectFiles(resolvedPath, "", includeEnvFiles ?? false);
|
package/package.json
CHANGED