zerg-ztc 0.1.0 → 0.1.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.
Files changed (42) hide show
  1. package/dist/App.d.ts.map +1 -1
  2. package/dist/App.js +16 -0
  3. package/dist/App.js.map +1 -1
  4. package/dist/cli.js +13 -8
  5. package/dist/cli.js.map +1 -1
  6. package/dist/components/InputArea.d.ts.map +1 -1
  7. package/dist/components/InputArea.js +9 -1
  8. package/dist/components/InputArea.js.map +1 -1
  9. package/dist/ui/views/input_area.js +1 -1
  10. package/dist/ui/web/frame_render.d.ts +23 -0
  11. package/dist/ui/web/frame_render.d.ts.map +1 -0
  12. package/dist/ui/web/frame_render.js +73 -0
  13. package/dist/ui/web/frame_render.js.map +1 -0
  14. package/dist/ui/web/index.d.ts +2 -0
  15. package/dist/ui/web/index.d.ts.map +1 -0
  16. package/dist/ui/web/index.js +2 -0
  17. package/dist/ui/web/index.js.map +1 -0
  18. package/dist/ui/web/render.d.ts +6 -0
  19. package/dist/ui/web/render.d.ts.map +1 -0
  20. package/dist/ui/web/render.js +30 -0
  21. package/dist/ui/web/render.js.map +1 -0
  22. package/dist/web/mirror_hook.d.ts +4 -0
  23. package/dist/web/mirror_hook.d.ts.map +1 -0
  24. package/dist/web/mirror_hook.js +22 -0
  25. package/dist/web/mirror_hook.js.map +1 -0
  26. package/dist/web/mirror_server.d.ts +16 -0
  27. package/dist/web/mirror_server.d.ts.map +1 -0
  28. package/dist/web/mirror_server.js +177 -0
  29. package/dist/web/mirror_server.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/App.tsx +18 -0
  32. package/src/cli.tsx +15 -8
  33. package/src/components/InputArea.tsx +9 -1
  34. package/src/ui/views/input_area.ts +1 -1
  35. package/src/ui/web/frame_render.tsx +148 -0
  36. package/src/ui/web/index.tsx +1 -0
  37. package/src/ui/web/render.tsx +41 -0
  38. package/src/web/index.html +352 -0
  39. package/src/web/mirror-favicon.svg +4 -0
  40. package/src/web/mirror.html +641 -0
  41. package/src/web/mirror_hook.ts +25 -0
  42. package/src/web/mirror_server.ts +204 -0
@@ -0,0 +1,204 @@
1
+ import { createServer, IncomingMessage, ServerResponse } from 'http';
2
+ import { readFile, mkdir, writeFile } from 'fs/promises';
3
+ import { resolve, join, basename } from 'path';
4
+ import { homedir } from 'os';
5
+ import { randomUUID } from 'crypto';
6
+ import { InputBus, InputEvent } from '../ui/core/input.js';
7
+ import { LayoutNode, computeLayout, LayoutFrame } from '../ui/core/index.js';
8
+
9
+ interface Client {
10
+ id: string;
11
+ res: ServerResponse;
12
+ cols: number;
13
+ rows: number;
14
+ }
15
+
16
+ export class MirrorServer {
17
+ private clients = new Map<string, Client>();
18
+ private port: number;
19
+ private server = createServer(this.handleRequest.bind(this));
20
+ private lastTree: LayoutNode | null = null;
21
+ private inputBus?: InputBus;
22
+
23
+ constructor(port: number, inputBus?: InputBus) {
24
+ this.port = port;
25
+ this.inputBus = inputBus;
26
+ }
27
+
28
+ start(): void {
29
+ this.server.listen(this.port);
30
+ }
31
+
32
+ stop(): void {
33
+ this.server.close();
34
+ }
35
+
36
+ publish(tree: LayoutNode): void {
37
+ this.lastTree = tree;
38
+ for (const client of this.clients.values()) {
39
+ this.sendLayout(client, tree);
40
+ }
41
+ }
42
+
43
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
44
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
45
+
46
+ if (url.pathname === '/') {
47
+ const htmlPath = new URL('./mirror.html', import.meta.url);
48
+ const html = await readFile(htmlPath, 'utf-8').catch(async () => {
49
+ const fallback = resolve(process.cwd(), 'src/web/mirror.html');
50
+ return readFile(fallback, 'utf-8');
51
+ });
52
+ res.writeHead(200, { 'Content-Type': 'text/html' });
53
+ res.end(html);
54
+ return;
55
+ }
56
+
57
+ if (url.pathname === '/mirror-favicon.svg' || url.pathname === '/favicon.svg' || url.pathname === '/favicon.ico') {
58
+ const iconPath = new URL('./mirror-favicon.svg', import.meta.url);
59
+ const icon = await readFile(iconPath, 'utf-8').catch(async () => {
60
+ const fallback = resolve(process.cwd(), 'src/web/mirror-favicon.svg');
61
+ return readFile(fallback, 'utf-8');
62
+ });
63
+ const contentType = url.pathname === '/favicon.ico' ? 'image/x-icon' : 'image/svg+xml';
64
+ res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
65
+ res.end(icon);
66
+ return;
67
+ }
68
+
69
+ if (url.pathname === '/events') {
70
+ const id = url.searchParams.get('id') || `client_${Date.now()}`;
71
+ res.writeHead(200, {
72
+ 'Content-Type': 'text/event-stream',
73
+ 'Cache-Control': 'no-cache',
74
+ Connection: 'keep-alive'
75
+ });
76
+ res.write('\n');
77
+
78
+ const client: Client = {
79
+ id,
80
+ res,
81
+ cols: 80,
82
+ rows: 24
83
+ };
84
+ this.clients.set(id, client);
85
+
86
+ if (this.lastTree) {
87
+ this.sendLayout(client, this.lastTree);
88
+ }
89
+
90
+ req.on('close', () => {
91
+ this.clients.delete(id);
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (url.pathname === '/size' && req.method === 'POST') {
97
+ const id = url.searchParams.get('id');
98
+ if (!id || !this.clients.has(id)) {
99
+ res.writeHead(400);
100
+ res.end();
101
+ return;
102
+ }
103
+
104
+ const body = await new Promise<string>(resolve => {
105
+ let data = '';
106
+ req.on('data', chunk => { data += chunk; });
107
+ req.on('end', () => resolve(data));
108
+ });
109
+
110
+ try {
111
+ const parsed = JSON.parse(body) as { cols: number; rows: number };
112
+ const client = this.clients.get(id);
113
+ if (client) {
114
+ client.cols = parsed.cols;
115
+ client.rows = parsed.rows;
116
+ if (this.lastTree) {
117
+ this.sendLayout(client, this.lastTree);
118
+ }
119
+ }
120
+ } catch {
121
+ // Ignore invalid payloads
122
+ }
123
+
124
+ res.writeHead(204);
125
+ res.end();
126
+ return;
127
+ }
128
+
129
+ if (url.pathname === '/input' && req.method === 'POST') {
130
+ const body = await new Promise<string>(resolve => {
131
+ let data = '';
132
+ req.on('data', chunk => { data += chunk; });
133
+ req.on('end', () => resolve(data));
134
+ });
135
+
136
+ try {
137
+ const parsed = JSON.parse(body) as InputEvent;
138
+ this.inputBus?.emit(parsed);
139
+ } catch {
140
+ // Ignore invalid payloads
141
+ }
142
+
143
+ res.writeHead(204);
144
+ res.end();
145
+ return;
146
+ }
147
+
148
+ if (url.pathname === '/upload' && req.method === 'POST') {
149
+ const buffer = await new Promise<Buffer>(resolve => {
150
+ const chunks: Buffer[] = [];
151
+ req.on('data', chunk => { chunks.push(Buffer.from(chunk)); });
152
+ req.on('end', () => resolve(Buffer.concat(chunks)));
153
+ });
154
+ const headerName = req.headers['x-filename'];
155
+ const rawName = typeof headerName === 'string' && headerName.trim().length > 0 ? headerName : 'upload.bin';
156
+ const safeName = basename(rawName).replace(/[^a-zA-Z0-9._-]/g, '_');
157
+ const dir = join(homedir(), '.ztc', 'uploads');
158
+ await mkdir(dir, { recursive: true });
159
+ const filePath = join(dir, `${Date.now()}-${randomUUID().slice(0, 8)}-${safeName}`);
160
+ await writeFile(filePath, buffer);
161
+ res.writeHead(200, { 'Content-Type': 'application/json' });
162
+ res.end(JSON.stringify({ path: filePath }));
163
+ return;
164
+ }
165
+
166
+ if (url.pathname === '/file' && req.method === 'GET') {
167
+ const rawPath = url.searchParams.get('path');
168
+ if (!rawPath) {
169
+ res.writeHead(400);
170
+ res.end();
171
+ return;
172
+ }
173
+ const allowedRoots = [
174
+ join(homedir(), '.ztc', 'uploads'),
175
+ join(homedir(), '.ztc', 'clipboard')
176
+ ];
177
+ const resolvedPath = resolve(rawPath);
178
+ const allowed = allowedRoots.some(root => resolvedPath.startsWith(resolve(root)));
179
+ if (!allowed) {
180
+ res.writeHead(403);
181
+ res.end();
182
+ return;
183
+ }
184
+ try {
185
+ const data = await readFile(resolvedPath);
186
+ res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
187
+ res.end(data);
188
+ } catch {
189
+ res.writeHead(404);
190
+ res.end();
191
+ }
192
+ return;
193
+ }
194
+
195
+ res.writeHead(404);
196
+ res.end();
197
+ }
198
+
199
+ private sendLayout(client: Client, tree: LayoutNode): void {
200
+ const frame = computeLayout(tree, client.cols, client.rows);
201
+ const payload = JSON.stringify(frame);
202
+ client.res.write(`data: ${payload}\n\n`);
203
+ }
204
+ }