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.
@@ -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 };