markui-cli 0.1.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 +32 -0
- package/bin/markui.js +146 -0
- package/package.json +36 -0
- package/src/injector.js +107 -0
- package/src/overlay/overlay.css +712 -0
- package/src/overlay/overlay.js +884 -0
- package/src/prompt-builder.js +24 -0
- package/src/proxy.js +250 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats annotations into a structured prompt for Claude Code.
|
|
3
|
+
* Used server-side if needed; the client-side overlay also builds prompts directly.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function buildPrompt(annotations) {
|
|
7
|
+
if (!annotations || annotations.length === 0) {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
var lines = ['Please make the following UI changes:'];
|
|
12
|
+
|
|
13
|
+
for (var i = 0; i < annotations.length; i++) {
|
|
14
|
+
var a = annotations[i];
|
|
15
|
+
lines.push('');
|
|
16
|
+
lines.push((i + 1) + '. Element: <' + a.tagName + '> \u2014 ' + a.selector);
|
|
17
|
+
lines.push(' Selector: ' + a.selector);
|
|
18
|
+
lines.push(' Change: ' + a.instruction);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return lines.join('\n') + '\n';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { buildPrompt };
|
package/src/proxy.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const zlib = require('zlib');
|
|
5
|
+
const httpProxy = require('http-proxy');
|
|
6
|
+
|
|
7
|
+
// In-memory annotation store keyed by page pathname
|
|
8
|
+
const annotations = {};
|
|
9
|
+
|
|
10
|
+
const OVERLAY_DIR = path.join(__dirname, 'overlay');
|
|
11
|
+
|
|
12
|
+
function readAsset(filename) {
|
|
13
|
+
return fs.readFileSync(path.join(OVERLAY_DIR, filename), 'utf8');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const INJECT_SNIPPET = `
|
|
17
|
+
<link rel="stylesheet" href="/__markui__/overlay.css">
|
|
18
|
+
<script src="/__markui__/overlay.js"></script>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
function startProxy({ targetPort, proxyPort }) {
|
|
22
|
+
const target = `http://127.0.0.1:${targetPort}`;
|
|
23
|
+
|
|
24
|
+
const proxy = httpProxy.createProxyServer({
|
|
25
|
+
target,
|
|
26
|
+
selfHandleResponse: true,
|
|
27
|
+
ws: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
proxy.on('error', (err, req, res) => {
|
|
31
|
+
if (res && res.writeHead && !res.headersSent) {
|
|
32
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
33
|
+
res.end(`MarkUI proxy error: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
proxy.on('proxyRes', (proxyRes, req, res) => {
|
|
38
|
+
// Copy status and headers, stripping CSP
|
|
39
|
+
const headers = { ...proxyRes.headers };
|
|
40
|
+
delete headers['content-security-policy'];
|
|
41
|
+
delete headers['content-security-policy-report-only'];
|
|
42
|
+
delete headers['x-content-security-policy'];
|
|
43
|
+
|
|
44
|
+
const isHtml = (headers['content-type'] || '').includes('text/html');
|
|
45
|
+
|
|
46
|
+
if (!isHtml) {
|
|
47
|
+
// Non-HTML: stream through directly
|
|
48
|
+
// Remove content-length since we're streaming
|
|
49
|
+
res.writeHead(proxyRes.statusCode, headers);
|
|
50
|
+
proxyRes.pipe(res);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// HTML: buffer, decompress if needed, inject, and send
|
|
55
|
+
const chunks = [];
|
|
56
|
+
proxyRes.on('data', (chunk) => chunks.push(chunk));
|
|
57
|
+
proxyRes.on('end', () => {
|
|
58
|
+
const raw = Buffer.concat(chunks);
|
|
59
|
+
const encoding = (proxyRes.headers['content-encoding'] || '').toLowerCase();
|
|
60
|
+
|
|
61
|
+
// Decompress if needed
|
|
62
|
+
let decompress;
|
|
63
|
+
if (encoding === 'gzip') {
|
|
64
|
+
decompress = zlib.gunzipSync;
|
|
65
|
+
} else if (encoding === 'br') {
|
|
66
|
+
decompress = zlib.brotliDecompressSync;
|
|
67
|
+
} else if (encoding === 'deflate') {
|
|
68
|
+
decompress = zlib.inflateSync;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let body;
|
|
72
|
+
try {
|
|
73
|
+
body = decompress ? decompress(raw).toString('utf8') : raw.toString('utf8');
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// If decompression fails, pass through unchanged
|
|
76
|
+
res.writeHead(proxyRes.statusCode, headers);
|
|
77
|
+
res.end(raw);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Inject before </body> if present, otherwise append
|
|
82
|
+
if (body.includes('</body>')) {
|
|
83
|
+
body = body.replace('</body>', INJECT_SNIPPET + '</body>');
|
|
84
|
+
} else if (body.includes('</html>')) {
|
|
85
|
+
body = body.replace('</html>', INJECT_SNIPPET + '</html>');
|
|
86
|
+
} else {
|
|
87
|
+
body += INJECT_SNIPPET;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Send uncompressed (simpler, avoids re-compression)
|
|
91
|
+
delete headers['content-length'];
|
|
92
|
+
delete headers['transfer-encoding'];
|
|
93
|
+
delete headers['content-encoding'];
|
|
94
|
+
const buf = Buffer.from(body, 'utf8');
|
|
95
|
+
headers['content-length'] = buf.length;
|
|
96
|
+
|
|
97
|
+
res.writeHead(proxyRes.statusCode, headers);
|
|
98
|
+
res.end(buf);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const server = http.createServer((req, res) => {
|
|
103
|
+
// Serve MarkUI's own assets and API
|
|
104
|
+
if (req.url.startsWith('/__markui__/')) {
|
|
105
|
+
return handleMarkuiRequest(req, res);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Request uncompressed HTML from target so injection is simpler
|
|
109
|
+
delete req.headers['accept-encoding'];
|
|
110
|
+
|
|
111
|
+
// Proxy everything else
|
|
112
|
+
proxy.web(req, res);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// WebSocket passthrough
|
|
116
|
+
server.on('upgrade', (req, socket, head) => {
|
|
117
|
+
proxy.ws(req, socket, head);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
server.listen(proxyPort, () => {
|
|
121
|
+
console.log(`
|
|
122
|
+
MarkUI running at http://localhost:${proxyPort}
|
|
123
|
+
Proxying → http://localhost:${targetPort}
|
|
124
|
+
Assets served at http://localhost:${proxyPort}/__markui__/overlay.js
|
|
125
|
+
|
|
126
|
+
Open http://localhost:${proxyPort} in your browser to start annotating.
|
|
127
|
+
Or use "markui inject" to add tags to your index.html and open localhost:${targetPort} directly.
|
|
128
|
+
`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Graceful shutdown
|
|
132
|
+
process.on('SIGINT', () => {
|
|
133
|
+
console.log('\nShutting down MarkUI...');
|
|
134
|
+
server.close();
|
|
135
|
+
process.exit(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
process.on('SIGTERM', () => {
|
|
139
|
+
server.close();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleMarkuiRequest(req, res) {
|
|
145
|
+
const url = req.url.split('?')[0];
|
|
146
|
+
|
|
147
|
+
const cors = {
|
|
148
|
+
'access-control-allow-origin': '*',
|
|
149
|
+
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
150
|
+
'access-control-allow-headers': 'content-type'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Static assets
|
|
154
|
+
if (url === '/__markui__/overlay.js') {
|
|
155
|
+
res.writeHead(200, { 'content-type': 'application/javascript; charset=utf-8', ...cors });
|
|
156
|
+
res.end(readAsset('overlay.js'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (url === '/__markui__/overlay.css') {
|
|
161
|
+
res.writeHead(200, { 'content-type': 'text/css; charset=utf-8', ...cors });
|
|
162
|
+
res.end(readAsset('overlay.css'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Annotations API
|
|
167
|
+
if (url === '/__markui__/annotations') {
|
|
168
|
+
if (req.method === 'GET') {
|
|
169
|
+
res.writeHead(200, {
|
|
170
|
+
'content-type': 'application/json',
|
|
171
|
+
'access-control-allow-origin': '*'
|
|
172
|
+
});
|
|
173
|
+
res.end(JSON.stringify(annotations));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (req.method === 'POST') {
|
|
178
|
+
let body = '';
|
|
179
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
180
|
+
req.on('end', () => {
|
|
181
|
+
try {
|
|
182
|
+
const data = JSON.parse(body);
|
|
183
|
+
const page = data.page || '/';
|
|
184
|
+
annotations[page] = data.annotations || [];
|
|
185
|
+
res.writeHead(200, {
|
|
186
|
+
'content-type': 'application/json',
|
|
187
|
+
'access-control-allow-origin': '*'
|
|
188
|
+
});
|
|
189
|
+
res.end(JSON.stringify({ ok: true }));
|
|
190
|
+
} catch (e) {
|
|
191
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
192
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// CORS preflight
|
|
199
|
+
if (req.method === 'OPTIONS') {
|
|
200
|
+
res.writeHead(204, {
|
|
201
|
+
'access-control-allow-origin': '*',
|
|
202
|
+
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
203
|
+
'access-control-allow-headers': 'content-type'
|
|
204
|
+
});
|
|
205
|
+
res.end();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
res.writeHead(404, { 'content-type': 'text/plain' });
|
|
211
|
+
res.end('Not found');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function startAssetServer({ port }) {
|
|
215
|
+
const server = http.createServer((req, res) => {
|
|
216
|
+
if (req.url.startsWith('/__markui__/')) {
|
|
217
|
+
return handleMarkuiRequest(req, res);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
221
|
+
res.end(
|
|
222
|
+
'<html><body style="font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;color:#666">' +
|
|
223
|
+
'<div style="text-align:center"><h2>MarkUI Asset Server</h2>' +
|
|
224
|
+
'<p>Assets are being served. Open your app to use MarkUI.</p></div></body></html>'
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
server.listen(port, () => {
|
|
229
|
+
console.log(`
|
|
230
|
+
MarkUI asset server running at http://localhost:${port}
|
|
231
|
+
Assets served at http://localhost:${port}/__markui__/overlay.js
|
|
232
|
+
|
|
233
|
+
Use "markui inject" to add markui to your index.html,
|
|
234
|
+
then open your app directly (e.g. http://localhost:3000).
|
|
235
|
+
`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
process.on('SIGINT', () => {
|
|
239
|
+
console.log('\nShutting down MarkUI...');
|
|
240
|
+
server.close();
|
|
241
|
+
process.exit(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
process.on('SIGTERM', () => {
|
|
245
|
+
server.close();
|
|
246
|
+
process.exit(0);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = { startProxy, startAssetServer };
|