@pixelbyte-software/pixcode 1.39.1 → 1.40.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/dist/assets/{index-B4DKVLz5.js → index-CrjtbuPH.js} +161 -160
- package/dist/assets/index-ErbnSPUf.css +32 -0
- package/dist/index.html +2 -2
- package/dist/landing.html +1 -1
- package/dist-server/server/index.js +4 -0
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/routes/live-view.js +170 -0
- package/dist-server/server/routes/live-view.js.map +1 -0
- package/dist-server/server/services/live-view.js +517 -0
- package/dist-server/server/services/live-view.js.map +1 -0
- package/package.json +1 -1
- package/scripts/smoke/chat-composer-fixed-layout.mjs +10 -0
- package/scripts/smoke/live-view-integration.mjs +99 -0
- package/server/index.js +5 -0
- package/server/routes/live-view.js +189 -0
- package/server/services/live-view.js +560 -0
- package/dist/assets/index-DoOHFfYP.css +0 -32
package/server/index.js
CHANGED
|
@@ -78,6 +78,7 @@ import messagesRoutes from './routes/messages.js';
|
|
|
78
78
|
import diagnosticsRoutes from './routes/diagnostics.js';
|
|
79
79
|
import remoteRoutes from './routes/remote.js';
|
|
80
80
|
import publicApiRoutes from './routes/public-api.js';
|
|
81
|
+
import liveViewRoutes, { createLiveViewPublicRouter } from './routes/live-view.js';
|
|
81
82
|
import providerRoutes from './modules/providers/provider.routes.js';
|
|
82
83
|
import {
|
|
83
84
|
createA2ARouter,
|
|
@@ -405,6 +406,9 @@ app.use('/api/remote', authenticateToken, remoteRoutes);
|
|
|
405
406
|
// Public automation manifest (protected so private host details only go to signed-in clients)
|
|
406
407
|
app.use('/api/public', authenticateToken, publicApiRoutes);
|
|
407
408
|
|
|
409
|
+
// Project Live View (protected control API + public share proxy)
|
|
410
|
+
app.use('/api/live-view', authenticateToken, liveViewRoutes);
|
|
411
|
+
|
|
408
412
|
// Unified provider MCP routes (protected)
|
|
409
413
|
app.use('/api/providers', authenticateToken, providerRoutes);
|
|
410
414
|
|
|
@@ -419,6 +423,7 @@ app.use('/a2a', createA2ARouter());
|
|
|
419
423
|
app.use('/preview', authenticateToken, createPreviewProxyRouter());
|
|
420
424
|
app.use('/api/orchestration', authenticateToken, createOrchestrationTaskRouter());
|
|
421
425
|
app.use('/api/orchestration', authenticateToken, createWorkflowRouter());
|
|
426
|
+
app.use('/live', createLiveViewPublicRouter());
|
|
422
427
|
|
|
423
428
|
// Network discovery / QR endpoints (protected)
|
|
424
429
|
app.use('/api/network', authenticateToken, networkRoutes);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import express from 'express';
|
|
5
|
+
|
|
6
|
+
import { extractProjectDirectory } from '../projects.js';
|
|
7
|
+
import { getTunnelState } from '../services/external-access.js';
|
|
8
|
+
import {
|
|
9
|
+
getLiveViewSessionByShareId,
|
|
10
|
+
getLiveViewState,
|
|
11
|
+
restartLiveView,
|
|
12
|
+
startLiveView,
|
|
13
|
+
stopLiveView,
|
|
14
|
+
} from '../services/live-view.js';
|
|
15
|
+
|
|
16
|
+
const router = express.Router();
|
|
17
|
+
|
|
18
|
+
function requestBaseUrl(req) {
|
|
19
|
+
return `${req.protocol}://${req.get('host')}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildUrls(req, session) {
|
|
23
|
+
if (!session?.sharePath) return { local: null, external: null, preferred: null };
|
|
24
|
+
const local = `${requestBaseUrl(req)}${session.sharePath}`;
|
|
25
|
+
const tunnel = getTunnelState();
|
|
26
|
+
const external = tunnel?.running && tunnel.url ? `${tunnel.url}${session.sharePath}` : null;
|
|
27
|
+
return {
|
|
28
|
+
local,
|
|
29
|
+
external,
|
|
30
|
+
preferred: external || local,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function resolveProjectPath(projectName) {
|
|
35
|
+
const projectPath = await extractProjectDirectory(projectName);
|
|
36
|
+
if (!projectPath || typeof projectPath !== 'string') {
|
|
37
|
+
const error = new Error('Project path could not be resolved.');
|
|
38
|
+
error.statusCode = 404;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return projectPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
router.get('/:projectName/status', async (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const { projectName } = req.params;
|
|
47
|
+
const projectPath = await resolveProjectPath(projectName);
|
|
48
|
+
const state = await getLiveViewState(projectName, projectPath);
|
|
49
|
+
res.json({
|
|
50
|
+
...state,
|
|
51
|
+
urls: buildUrls(req, state.session),
|
|
52
|
+
tunnel: getTunnelState(),
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
res.status(error.statusCode || 500).json({ error: error.message || 'Failed to read Live View state' });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.post('/:projectName/start', async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const { projectName } = req.params;
|
|
62
|
+
const projectPath = await resolveProjectPath(projectName);
|
|
63
|
+
const session = await startLiveView(projectName, projectPath, req.body || {});
|
|
64
|
+
res.json({
|
|
65
|
+
success: true,
|
|
66
|
+
session,
|
|
67
|
+
urls: buildUrls(req, session),
|
|
68
|
+
tunnel: getTunnelState(),
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const status = error.code === 'LIVE_VIEW_NOT_AVAILABLE' ? 422 : 500;
|
|
72
|
+
res.status(status).json({ error: error.message || 'Failed to start Live View' });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
router.post('/:projectName/restart', async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { projectName } = req.params;
|
|
79
|
+
const projectPath = await resolveProjectPath(projectName);
|
|
80
|
+
const session = await restartLiveView(projectName, projectPath, req.body || {});
|
|
81
|
+
res.json({
|
|
82
|
+
success: true,
|
|
83
|
+
session,
|
|
84
|
+
urls: buildUrls(req, session),
|
|
85
|
+
tunnel: getTunnelState(),
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
res.status(500).json({ error: error.message || 'Failed to restart Live View' });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
router.post('/:projectName/stop', async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const session = await stopLiveView(req.params.projectName);
|
|
95
|
+
res.json({
|
|
96
|
+
success: true,
|
|
97
|
+
session,
|
|
98
|
+
urls: buildUrls(req, session),
|
|
99
|
+
tunnel: getTunnelState(),
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
res.status(500).json({ error: error.message || 'Failed to stop Live View' });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function resolveStaticFile(staticRoot, requestUrl) {
|
|
107
|
+
const parsed = new URL(requestUrl, 'http://pixcode.local');
|
|
108
|
+
const rawPath = decodeURIComponent(parsed.pathname || '/');
|
|
109
|
+
const relativePath = rawPath.replace(/^\/+/, '') || 'index.html';
|
|
110
|
+
const root = path.resolve(staticRoot);
|
|
111
|
+
const candidate = path.resolve(root, relativePath);
|
|
112
|
+
const relative = path.relative(root, candidate);
|
|
113
|
+
|
|
114
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
115
|
+
const error = new Error('Live View path is outside the static root.');
|
|
116
|
+
error.statusCode = 403;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function sendStaticLiveView(req, res, session) {
|
|
124
|
+
const root = session.staticRoot;
|
|
125
|
+
let filePath = resolveStaticFile(root, req.url);
|
|
126
|
+
try {
|
|
127
|
+
const stats = await fs.stat(filePath);
|
|
128
|
+
if (stats.isDirectory()) {
|
|
129
|
+
filePath = path.join(filePath, 'index.html');
|
|
130
|
+
}
|
|
131
|
+
await fs.access(filePath);
|
|
132
|
+
} catch {
|
|
133
|
+
filePath = path.join(root, 'index.html');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
137
|
+
res.sendFile(filePath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function proxyLiveView(req, res, session) {
|
|
141
|
+
if (!session.upstreamUrl) {
|
|
142
|
+
res.status(503).json({ error: 'Live View upstream is not ready yet.' });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const targetUrl = new URL(req.url || '/', session.upstreamUrl);
|
|
147
|
+
try {
|
|
148
|
+
const upstream = await fetch(targetUrl, {
|
|
149
|
+
method: req.method,
|
|
150
|
+
headers: {
|
|
151
|
+
accept: req.header('accept') || '*/*',
|
|
152
|
+
'user-agent': req.header('user-agent') || 'pixcode-live-view',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
res.status(upstream.status);
|
|
156
|
+
upstream.headers.forEach((value, key) => {
|
|
157
|
+
const lower = key.toLowerCase();
|
|
158
|
+
if (lower === 'content-encoding' || lower === 'content-length') return;
|
|
159
|
+
if (lower === 'x-frame-options' || lower === 'content-security-policy') return;
|
|
160
|
+
res.setHeader(key, value);
|
|
161
|
+
});
|
|
162
|
+
res.send(Buffer.from(await upstream.arrayBuffer()));
|
|
163
|
+
} catch (error) {
|
|
164
|
+
res.status(502).json({ error: error.message || 'Live View proxy failed' });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function createLiveViewPublicRouter() {
|
|
169
|
+
const publicRouter = express.Router();
|
|
170
|
+
|
|
171
|
+
publicRouter.use('/:shareId', async (req, res) => {
|
|
172
|
+
const session = getLiveViewSessionByShareId(req.params.shareId);
|
|
173
|
+
if (!session || session.status === 'stopped' || session.status === 'error') {
|
|
174
|
+
res.status(404).json({ error: 'Live View session not found.' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (session.kind === 'static') {
|
|
179
|
+
await sendStaticLiveView(req, res, session);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await proxyLiveView(req, res, session);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return publicRouter;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default router;
|