zelune 1.6.2 → 1.6.4
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/CHANGELOG.md +16 -0
- package/bin/cli.mjs +13 -54
- package/dist/assets/CheckoutScreen-6IdrpKgm.js +229 -0
- package/dist/assets/HomeScreen-Dxe9L4I2.js +316 -0
- package/dist/assets/LoginScreen-Dp1awI4F.js +189 -0
- package/dist/assets/ProductDetailScreen-Wa5_nlRP.js +196 -0
- package/dist/assets/ProfileScreen-riAU6vVh.js +185 -0
- package/dist/assets/index-hEzd4JVy.js +44697 -0
- package/{index.html → dist/index.html} +3 -7
- package/mcp/server.mjs +1370 -34
- package/package.json +10 -17
- package/plugin.json +1 -1
- package/socket.yml +11 -0
- package/mcp/server.ts +0 -446
- package/plugins/viteReactNativeFallbackPlugin.ts +0 -519
- package/plugins/viteRnSourceInject.ts +0 -117
- package/plugins/viteVirtualScreensPlugin.ts +0 -216
- package/src/canvas/CanvasApp.tsx +0 -1655
- package/src/canvas/FlowArrowsLayer.tsx +0 -239
- package/src/canvas/InspectorPanel.tsx +0 -1494
- package/src/canvas/ScreenArtboard.tsx +0 -1491
- package/src/canvas/types.ts +0 -23
- package/src/main.tsx +0 -80
- package/src/runtime/MockNavigationContext.tsx +0 -1795
- package/src/runtime/ScreenErrorBoundary.tsx +0 -86
- package/src/runtime/nativewindShim.ts +0 -997
- package/src/runtime/reactNativeShim.ts +0 -240
- package/src/screens/CheckoutScreen.tsx +0 -229
- package/src/screens/HomeScreen.tsx +0 -288
- package/src/screens/LoginScreen.tsx +0 -176
- package/src/screens/ProductDetailScreen.tsx +0 -207
- package/src/screens/ProfileScreen.tsx +0 -172
- package/src/screens/index.ts +0 -1
- package/src/server/apiMiddleware.ts +0 -375
- package/src/server/astEngine.ts +0 -2553
- package/src/vite-env.d.ts +0 -7
- package/tsconfig.json +0 -23
- package/vite.config.ts +0 -340
- /package/{src/styles.css → dist/assets/index-C7nrS82N.css} +0 -0
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
View,
|
|
4
|
-
Text,
|
|
5
|
-
TouchableOpacity,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
} from 'react-native';
|
|
8
|
-
import { useNavigation } from '../runtime/MockNavigationContext';
|
|
9
|
-
|
|
10
|
-
export default function ProfileScreen() {
|
|
11
|
-
const navigation = useNavigation();
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<View style={styles.container}>
|
|
15
|
-
<View style={styles.profileHeader}>
|
|
16
|
-
<View style={styles.avatarLarge}>
|
|
17
|
-
<Text style={styles.avatarInitials}>AX</Text>
|
|
18
|
-
</View>
|
|
19
|
-
<Text style={styles.memberName}>Alex Mercer</Text>
|
|
20
|
-
<Text style={styles.memberTier}>LEAD ACOUSTIC ENGINEER · TIER 01</Text>
|
|
21
|
-
</View>
|
|
22
|
-
|
|
23
|
-
<View style={styles.statsRow}>
|
|
24
|
-
<View style={styles.statCard}>
|
|
25
|
-
<Text style={styles.statNumber}>14</Text>
|
|
26
|
-
<Text style={styles.statCaption}>UNITS</Text>
|
|
27
|
-
</View>
|
|
28
|
-
<View style={styles.statCard}>
|
|
29
|
-
<Text style={styles.statNumber}>4,820</Text>
|
|
30
|
-
<Text style={styles.statCaption}>CREDITS</Text>
|
|
31
|
-
</View>
|
|
32
|
-
<View style={styles.statCard}>
|
|
33
|
-
<Text style={styles.statNumber}>LAB</Text>
|
|
34
|
-
<Text style={styles.statCaption}>ACCESS</Text>
|
|
35
|
-
</View>
|
|
36
|
-
</View>
|
|
37
|
-
|
|
38
|
-
<View style={styles.menuList}>
|
|
39
|
-
<TouchableOpacity
|
|
40
|
-
activeOpacity={0.78}
|
|
41
|
-
style={styles.menuItem}
|
|
42
|
-
onPress={() => navigation.navigate('Home')}
|
|
43
|
-
>
|
|
44
|
-
<Text style={styles.menuItemTitle}>Reference Hardware Catalog</Text>
|
|
45
|
-
<Text style={styles.menuItemArrow}>→</Text>
|
|
46
|
-
</TouchableOpacity>
|
|
47
|
-
|
|
48
|
-
<TouchableOpacity
|
|
49
|
-
activeOpacity={0.78}
|
|
50
|
-
style={styles.menuItem}
|
|
51
|
-
onPress={() => navigation.navigate('Checkout')}
|
|
52
|
-
>
|
|
53
|
-
<Text style={styles.menuItemTitle}>Active Allocation & Settlement</Text>
|
|
54
|
-
<Text style={styles.menuItemArrow}>→</Text>
|
|
55
|
-
</TouchableOpacity>
|
|
56
|
-
</View>
|
|
57
|
-
|
|
58
|
-
<TouchableOpacity
|
|
59
|
-
activeOpacity={0.78}
|
|
60
|
-
style={styles.signOutButton}
|
|
61
|
-
onPress={() => navigation.navigate('Login')}
|
|
62
|
-
>
|
|
63
|
-
<Text style={styles.signOutText}>Terminate Studio Session</Text>
|
|
64
|
-
</TouchableOpacity>
|
|
65
|
-
</View>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const styles = StyleSheet.create({
|
|
70
|
-
container: {
|
|
71
|
-
flex: 1,
|
|
72
|
-
backgroundColor: '#0A0B0E',
|
|
73
|
-
padding: 20,
|
|
74
|
-
justifyContent: 'space-between',
|
|
75
|
-
},
|
|
76
|
-
profileHeader: {
|
|
77
|
-
alignItems: 'center',
|
|
78
|
-
marginTop: 14,
|
|
79
|
-
gap: 6,
|
|
80
|
-
},
|
|
81
|
-
avatarLarge: {
|
|
82
|
-
width: 68,
|
|
83
|
-
height: 68,
|
|
84
|
-
borderRadius: 16,
|
|
85
|
-
backgroundColor: '#13161E',
|
|
86
|
-
borderWidth: 1,
|
|
87
|
-
borderColor: '#262D3D',
|
|
88
|
-
alignItems: 'center',
|
|
89
|
-
justifyContent: 'center',
|
|
90
|
-
marginBottom: 6,
|
|
91
|
-
},
|
|
92
|
-
avatarInitials: {
|
|
93
|
-
color: '#38BDF8',
|
|
94
|
-
fontSize: 22,
|
|
95
|
-
fontWeight: '700',
|
|
96
|
-
letterSpacing: 1,
|
|
97
|
-
},
|
|
98
|
-
memberName: {
|
|
99
|
-
color: '#F8FAFC',
|
|
100
|
-
fontSize: 22,
|
|
101
|
-
fontWeight: '700',
|
|
102
|
-
letterSpacing: -0.3,
|
|
103
|
-
},
|
|
104
|
-
memberTier: {
|
|
105
|
-
color: '#64748B',
|
|
106
|
-
fontSize: 10,
|
|
107
|
-
fontWeight: '700',
|
|
108
|
-
letterSpacing: 1,
|
|
109
|
-
},
|
|
110
|
-
statsRow: {
|
|
111
|
-
flexDirection: 'row',
|
|
112
|
-
gap: 8,
|
|
113
|
-
},
|
|
114
|
-
statCard: {
|
|
115
|
-
flex: 1,
|
|
116
|
-
backgroundColor: '#11141C',
|
|
117
|
-
borderRadius: 12,
|
|
118
|
-
paddingVertical: 14,
|
|
119
|
-
alignItems: 'center',
|
|
120
|
-
borderWidth: 1,
|
|
121
|
-
borderColor: '#1E2433',
|
|
122
|
-
},
|
|
123
|
-
statNumber: {
|
|
124
|
-
color: '#F8FAFC',
|
|
125
|
-
fontSize: 17,
|
|
126
|
-
fontWeight: '700',
|
|
127
|
-
},
|
|
128
|
-
statCaption: {
|
|
129
|
-
color: '#64748B',
|
|
130
|
-
fontSize: 9,
|
|
131
|
-
fontWeight: '700',
|
|
132
|
-
letterSpacing: 0.8,
|
|
133
|
-
marginTop: 3,
|
|
134
|
-
},
|
|
135
|
-
menuList: {
|
|
136
|
-
gap: 10,
|
|
137
|
-
},
|
|
138
|
-
menuItem: {
|
|
139
|
-
backgroundColor: '#11141C',
|
|
140
|
-
borderRadius: 12,
|
|
141
|
-
paddingHorizontal: 16,
|
|
142
|
-
paddingVertical: 15,
|
|
143
|
-
flexDirection: 'row',
|
|
144
|
-
justifyContent: 'space-between',
|
|
145
|
-
alignItems: 'center',
|
|
146
|
-
borderWidth: 1,
|
|
147
|
-
borderColor: '#1E2433',
|
|
148
|
-
},
|
|
149
|
-
menuItemTitle: {
|
|
150
|
-
color: '#E2E8F0',
|
|
151
|
-
fontSize: 13,
|
|
152
|
-
fontWeight: '600',
|
|
153
|
-
},
|
|
154
|
-
menuItemArrow: {
|
|
155
|
-
color: '#38BDF8',
|
|
156
|
-
fontSize: 15,
|
|
157
|
-
fontWeight: '700',
|
|
158
|
-
},
|
|
159
|
-
signOutButton: {
|
|
160
|
-
backgroundColor: '#16131A',
|
|
161
|
-
borderRadius: 10,
|
|
162
|
-
paddingVertical: 14,
|
|
163
|
-
alignItems: 'center',
|
|
164
|
-
borderWidth: 1,
|
|
165
|
-
borderColor: '#3B1D2A',
|
|
166
|
-
},
|
|
167
|
-
signOutText: {
|
|
168
|
-
color: '#FB7185',
|
|
169
|
-
fontSize: 13,
|
|
170
|
-
fontWeight: '600',
|
|
171
|
-
},
|
|
172
|
-
});
|
package/src/screens/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { SCREEN_COMPONENTS, SCREEN_ORDER } from 'virtual:zelune-screens';
|
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
-
import type { Plugin } from 'vite';
|
|
5
|
-
import {
|
|
6
|
-
discoverScreens,
|
|
7
|
-
getActiveProjectRoot,
|
|
8
|
-
inspectElement,
|
|
9
|
-
patchElement,
|
|
10
|
-
reorderElement,
|
|
11
|
-
insertElement,
|
|
12
|
-
duplicateElement,
|
|
13
|
-
deleteElement,
|
|
14
|
-
undoLastPatch,
|
|
15
|
-
redoLastPatch,
|
|
16
|
-
getHistoryStatus,
|
|
17
|
-
type PatchPayload,
|
|
18
|
-
type ReorderPayload,
|
|
19
|
-
type InsertElementPayload,
|
|
20
|
-
type DuplicateElementPayload,
|
|
21
|
-
type DeleteElementPayload,
|
|
22
|
-
} from './astEngine';
|
|
23
|
-
|
|
24
|
-
const MAX_BODY_BYTES = 1_000_000;
|
|
25
|
-
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]', '::1']);
|
|
26
|
-
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
27
|
-
|
|
28
|
-
export function isLoopbackHostHeader(hostHeader: string | undefined): boolean {
|
|
29
|
-
if (!hostHeader) return false;
|
|
30
|
-
const trimmed = hostHeader.trim().toLowerCase();
|
|
31
|
-
if (trimmed.startsWith('[')) {
|
|
32
|
-
const closeIdx = trimmed.indexOf(']');
|
|
33
|
-
if (closeIdx === -1) return false;
|
|
34
|
-
const ipv6Host = trimmed.slice(0, closeIdx + 1);
|
|
35
|
-
return LOOPBACK_HOSTS.has(ipv6Host);
|
|
36
|
-
}
|
|
37
|
-
const hostname = trimmed.split(':')[0];
|
|
38
|
-
return LOOPBACK_HOSTS.has(hostname);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function verifyApiRequestSecurity(req: {
|
|
42
|
-
method?: string;
|
|
43
|
-
headers: Record<string, string | string[] | undefined>;
|
|
44
|
-
}): { allowed: boolean; reason?: string } {
|
|
45
|
-
const host = typeof req.headers.host === 'string' ? req.headers.host : undefined;
|
|
46
|
-
if (!isLoopbackHostHeader(host)) {
|
|
47
|
-
return {
|
|
48
|
-
allowed: false,
|
|
49
|
-
reason: 'Forbidden Host header: Local API only accepts loopback connections (127.0.0.1, localhost, [::1])',
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const secFetchSite =
|
|
54
|
-
typeof req.headers['sec-fetch-site'] === 'string'
|
|
55
|
-
? req.headers['sec-fetch-site'].toLowerCase()
|
|
56
|
-
: undefined;
|
|
57
|
-
if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'none') {
|
|
58
|
-
return {
|
|
59
|
-
allowed: false,
|
|
60
|
-
reason: `Forbidden cross-site request (Sec-Fetch-Site: ${secFetchSite})`,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
|
|
65
|
-
if (origin) {
|
|
66
|
-
try {
|
|
67
|
-
const parsedOrigin = new URL(origin);
|
|
68
|
-
if (!LOOPBACK_HOSTS.has(parsedOrigin.hostname.toLowerCase())) {
|
|
69
|
-
return {
|
|
70
|
-
allowed: false,
|
|
71
|
-
reason: `Forbidden cross-origin request from ${origin}`,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
return {
|
|
76
|
-
allowed: false,
|
|
77
|
-
reason: 'Malformed Origin header',
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (req.method === 'POST') {
|
|
83
|
-
const contentType =
|
|
84
|
-
typeof req.headers['content-type'] === 'string'
|
|
85
|
-
? req.headers['content-type'].toLowerCase()
|
|
86
|
-
: '';
|
|
87
|
-
if (!contentType.startsWith('application/json')) {
|
|
88
|
-
return {
|
|
89
|
-
allowed: false,
|
|
90
|
-
reason: 'POST requests to /api/* must specify Content-Type: application/json',
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { allowed: true };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function sanitizeLayoutPayload(
|
|
99
|
-
input: unknown
|
|
100
|
-
): Record<string, { x: number; y: number; width: number; height: number }> {
|
|
101
|
-
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
102
|
-
throw new Error('Invalid layout payload: expected an object map of screen coordinates');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const sanitized: Record<string, { x: number; y: number; width: number; height: number }> =
|
|
106
|
-
Object.create(null);
|
|
107
|
-
const entries = Object.entries(input as Record<string, unknown>);
|
|
108
|
-
if (entries.length > 200) {
|
|
109
|
-
throw new Error('Invalid layout payload: too many screen entries');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
for (const [screenId, rawBox] of entries) {
|
|
113
|
-
if (!screenId || FORBIDDEN_KEYS.has(screenId) || screenId.length > 120) {
|
|
114
|
-
throw new Error(`Invalid screen key in layout payload: "${screenId}"`);
|
|
115
|
-
}
|
|
116
|
-
if (!rawBox || typeof rawBox !== 'object' || Array.isArray(rawBox)) {
|
|
117
|
-
throw new Error(`Invalid layout coordinates for screen "${screenId}"`);
|
|
118
|
-
}
|
|
119
|
-
const box = rawBox as Record<string, unknown>;
|
|
120
|
-
const x = Number(box.x);
|
|
121
|
-
const y = Number(box.y);
|
|
122
|
-
const width = Number(box.width);
|
|
123
|
-
const height = Number(box.height);
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
!Number.isFinite(x) ||
|
|
127
|
-
!Number.isFinite(y) ||
|
|
128
|
-
!Number.isFinite(width) ||
|
|
129
|
-
!Number.isFinite(height) ||
|
|
130
|
-
Math.abs(x) > 100_000 ||
|
|
131
|
-
Math.abs(y) > 100_000 ||
|
|
132
|
-
width < 100 ||
|
|
133
|
-
width > 5_000 ||
|
|
134
|
-
height < 100 ||
|
|
135
|
-
height > 5_000
|
|
136
|
-
) {
|
|
137
|
-
throw new Error(`Out-of-bounds layout coordinates for screen "${screenId}"`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
sanitized[screenId] = {
|
|
141
|
-
x: Math.round(x),
|
|
142
|
-
y: Math.round(y),
|
|
143
|
-
width: Math.round(width),
|
|
144
|
-
height: Math.round(height),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return sanitized;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
|
|
152
|
-
return new Promise((resolve, reject) => {
|
|
153
|
-
let raw = '';
|
|
154
|
-
let byteCount = 0;
|
|
155
|
-
let aborted = false;
|
|
156
|
-
|
|
157
|
-
req.on('data', (chunk: Buffer | string) => {
|
|
158
|
-
if (aborted) return;
|
|
159
|
-
const chunkBytes =
|
|
160
|
-
typeof chunk === 'string' ? Buffer.byteLength(chunk, 'utf-8') : chunk.length;
|
|
161
|
-
byteCount += chunkBytes;
|
|
162
|
-
|
|
163
|
-
if (byteCount > MAX_BODY_BYTES) {
|
|
164
|
-
aborted = true;
|
|
165
|
-
req.destroy();
|
|
166
|
-
reject(new Error('Request payload too large'));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
raw += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
req.on('end', () => {
|
|
174
|
-
if (aborted) return;
|
|
175
|
-
try {
|
|
176
|
-
resolve(raw ? (JSON.parse(raw) as T) : ({} as T));
|
|
177
|
-
} catch (err) {
|
|
178
|
-
reject(err);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
req.on('error', (err) => {
|
|
183
|
-
if (!aborted) {
|
|
184
|
-
aborted = true;
|
|
185
|
-
reject(err);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function sendJson(res: ServerResponse, status: number, payload: unknown) {
|
|
192
|
-
res.statusCode = status;
|
|
193
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
194
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
195
|
-
res.end(JSON.stringify(payload));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function viteCanvasApiMiddleware(workspaceRoot: string): Plugin {
|
|
199
|
-
async function resolveConnectedProjectName(): Promise<string> {
|
|
200
|
-
const activeRoot = getActiveProjectRoot(workspaceRoot);
|
|
201
|
-
const dirName = path.basename(activeRoot);
|
|
202
|
-
try {
|
|
203
|
-
const appJsonRaw = await fs.readFile(path.join(activeRoot, 'app.json'), 'utf-8');
|
|
204
|
-
const appJson = JSON.parse(appJsonRaw) as {
|
|
205
|
-
expo?: { name?: string };
|
|
206
|
-
name?: string;
|
|
207
|
-
};
|
|
208
|
-
if (appJson?.expo?.name) return appJson.expo.name;
|
|
209
|
-
if (appJson?.name) return appJson.name;
|
|
210
|
-
} catch {
|
|
211
|
-
// No app.json present
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
const pkgRaw = await fs.readFile(path.join(activeRoot, 'package.json'), 'utf-8');
|
|
215
|
-
const pkg = JSON.parse(pkgRaw) as { name?: string };
|
|
216
|
-
if (pkg?.name && pkg.name.toLowerCase() !== 'zelune') {
|
|
217
|
-
return pkg.name;
|
|
218
|
-
}
|
|
219
|
-
} catch {
|
|
220
|
-
// Fallback to directory name
|
|
221
|
-
}
|
|
222
|
-
return dirName;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
name: 'vite-canvas-api-middleware',
|
|
227
|
-
configureServer(server) {
|
|
228
|
-
server.middlewares.use(async (req, res, next) => {
|
|
229
|
-
if (!req.url || !req.url.startsWith('/api/')) {
|
|
230
|
-
return next();
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const securityCheck = verifyApiRequestSecurity(req);
|
|
234
|
-
if (!securityCheck.allowed) {
|
|
235
|
-
return sendJson(res, 403, { ok: false, error: securityCheck.reason });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const activeRoot = getActiveProjectRoot(workspaceRoot);
|
|
239
|
-
const layoutFilePath = path.resolve(activeRoot, '.canvas-layout.json');
|
|
240
|
-
const urlObj = new URL(req.url, 'http://127.0.0.1');
|
|
241
|
-
const pathname = urlObj.pathname;
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
// GET /api/screens
|
|
245
|
-
if (req.method === 'GET' && pathname === '/api/screens') {
|
|
246
|
-
const [screens, projectName] = await Promise.all([
|
|
247
|
-
discoverScreens(workspaceRoot),
|
|
248
|
-
resolveConnectedProjectName(),
|
|
249
|
-
]);
|
|
250
|
-
const history = getHistoryStatus();
|
|
251
|
-
return sendJson(res, 200, { ok: true, projectName, screens, history });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// GET /api/inspect?file=...&astPath=...&line=...
|
|
255
|
-
if (req.method === 'GET' && pathname === '/api/inspect') {
|
|
256
|
-
const file = urlObj.searchParams.get('file') ?? '';
|
|
257
|
-
const astPath = urlObj.searchParams.get('astPath') ?? '';
|
|
258
|
-
const lineParam = urlObj.searchParams.get('line');
|
|
259
|
-
const line = lineParam ? Number.parseInt(lineParam, 10) : undefined;
|
|
260
|
-
|
|
261
|
-
const element = await inspectElement(workspaceRoot, { file, astPath, line });
|
|
262
|
-
const history = getHistoryStatus();
|
|
263
|
-
return sendJson(res, 200, { ok: true, element, history });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// POST /api/patch
|
|
267
|
-
if (req.method === 'POST' && pathname === '/api/patch') {
|
|
268
|
-
const body = await readJsonBody<PatchPayload>(req);
|
|
269
|
-
const result = await patchElement(workspaceRoot, body);
|
|
270
|
-
return sendJson(res, 200, {
|
|
271
|
-
ok: true,
|
|
272
|
-
element: result.element,
|
|
273
|
-
history: {
|
|
274
|
-
undoAvailable: result.undoAvailable,
|
|
275
|
-
redoAvailable: result.redoAvailable,
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// POST /api/reorder
|
|
281
|
-
if (req.method === 'POST' && pathname === '/api/reorder') {
|
|
282
|
-
const body = await readJsonBody<ReorderPayload>(req);
|
|
283
|
-
const result = await reorderElement(workspaceRoot, body);
|
|
284
|
-
return sendJson(res, 200, {
|
|
285
|
-
ok: true,
|
|
286
|
-
element: result.element,
|
|
287
|
-
history: {
|
|
288
|
-
undoAvailable: result.undoAvailable,
|
|
289
|
-
redoAvailable: result.redoAvailable,
|
|
290
|
-
},
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// POST /api/insert
|
|
295
|
-
if (req.method === 'POST' && pathname === '/api/insert') {
|
|
296
|
-
const body = await readJsonBody<InsertElementPayload>(req);
|
|
297
|
-
const result = await insertElement(workspaceRoot, body);
|
|
298
|
-
return sendJson(res, 200, {
|
|
299
|
-
ok: true,
|
|
300
|
-
element: result.element,
|
|
301
|
-
history: {
|
|
302
|
-
undoAvailable: result.undoAvailable,
|
|
303
|
-
redoAvailable: result.redoAvailable,
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// POST /api/duplicate
|
|
309
|
-
if (req.method === 'POST' && pathname === '/api/duplicate') {
|
|
310
|
-
const body = await readJsonBody<DuplicateElementPayload>(req);
|
|
311
|
-
const result = await duplicateElement(workspaceRoot, body);
|
|
312
|
-
return sendJson(res, 200, {
|
|
313
|
-
ok: true,
|
|
314
|
-
element: result.element,
|
|
315
|
-
history: {
|
|
316
|
-
undoAvailable: result.undoAvailable,
|
|
317
|
-
redoAvailable: result.redoAvailable,
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// POST /api/delete
|
|
323
|
-
if (req.method === 'POST' && pathname === '/api/delete') {
|
|
324
|
-
const body = await readJsonBody<DeleteElementPayload>(req);
|
|
325
|
-
const result = await deleteElement(workspaceRoot, body);
|
|
326
|
-
return sendJson(res, 200, {
|
|
327
|
-
ok: true,
|
|
328
|
-
deletedAstPath: result.deletedAstPath,
|
|
329
|
-
history: {
|
|
330
|
-
undoAvailable: result.undoAvailable,
|
|
331
|
-
redoAvailable: result.redoAvailable,
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// POST /api/undo
|
|
337
|
-
if (req.method === 'POST' && pathname === '/api/undo') {
|
|
338
|
-
const result = await undoLastPatch(workspaceRoot);
|
|
339
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// POST /api/redo
|
|
343
|
-
if (req.method === 'POST' && pathname === '/api/redo') {
|
|
344
|
-
const result = await redoLastPatch(workspaceRoot);
|
|
345
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// GET /api/layout
|
|
349
|
-
if (req.method === 'GET' && pathname === '/api/layout') {
|
|
350
|
-
try {
|
|
351
|
-
const raw = await fs.readFile(layoutFilePath, 'utf-8');
|
|
352
|
-
const validated = sanitizeLayoutPayload(JSON.parse(raw));
|
|
353
|
-
return sendJson(res, 200, { ok: true, layout: validated });
|
|
354
|
-
} catch {
|
|
355
|
-
return sendJson(res, 200, { ok: true, layout: null });
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// POST /api/layout
|
|
360
|
-
if (req.method === 'POST' && pathname === '/api/layout') {
|
|
361
|
-
const body = await readJsonBody<Record<string, unknown>>(req);
|
|
362
|
-
const validated = sanitizeLayoutPayload(body);
|
|
363
|
-
await fs.writeFile(layoutFilePath, JSON.stringify(validated, null, 2), 'utf-8');
|
|
364
|
-
return sendJson(res, 200, { ok: true });
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return sendJson(res, 404, { ok: false, error: `Unknown API route: ${pathname}` });
|
|
368
|
-
} catch (err) {
|
|
369
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
370
|
-
return sendJson(res, 400, { ok: false, error: message });
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
}
|