pi-deck 0.1.0 → 0.1.1
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 +6 -3
- package/bin/pi-deck.js +286 -50
- package/dist/server.js +74 -72
- package/dist/server.js.map +3 -3
- package/package.json +2 -1
- package/packages/client/dist/assets/MarkdownContent-D7i8glJy.js +2 -0
- package/packages/client/dist/assets/{index-BIGMLGdI.js → index-C2Zq6AKt.js} +139 -129
- package/packages/client/dist/assets/index-vhbgYHbc.css +1 -0
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/assets/MarkdownContent-BP8uvTu3.js +0 -2
- package/packages/client/dist/assets/index-1WD5ZoeL.css +0 -1
package/README.md
CHANGED
|
@@ -85,11 +85,14 @@ To remove: `npm unlink -g pi-deck`
|
|
|
85
85
|
## Publishing to npm
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
npm run
|
|
89
|
-
npm
|
|
90
|
-
npm
|
|
88
|
+
npm run publish:npm patch # 0.1.0 → 0.1.1
|
|
89
|
+
npm run publish:npm minor # 0.1.0 → 0.2.0
|
|
90
|
+
npm run publish:npm major # 0.1.0 → 1.0.0
|
|
91
|
+
npm run publish:npm patch -- --dry-run # preview without publishing
|
|
91
92
|
```
|
|
92
93
|
|
|
94
|
+
The script will: check for a clean working tree on `main`, bump the version in all `package.json` files, build, run tests, publish to npm, commit, tag, and push.
|
|
95
|
+
|
|
93
96
|
The published package includes a bundled server (`dist/server.js`) and the pre-built client SPA (`packages/client/dist/`). Workspace dependencies are inlined by esbuild; only runtime dependencies (`express`, `better-sqlite3`, etc.) are installed by npm.
|
|
94
97
|
|
|
95
98
|
## Configuration
|
package/bin/pi-deck.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, join, resolve } from 'path';
|
|
5
|
-
import { existsSync } from 'fs';
|
|
4
|
+
import { dirname, join, resolve, extname } from 'path';
|
|
5
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
6
6
|
import { execSync, fork } from 'child_process';
|
|
7
|
+
import { createServer, request as httpRequest } from 'http';
|
|
8
|
+
import { request as httpsRequest } from 'https';
|
|
7
9
|
|
|
8
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
11
|
const __dirname = dirname(__filename);
|
|
@@ -12,75 +14,309 @@ const ROOT = resolve(__dirname, '..');
|
|
|
12
14
|
const bundledServer = join(ROOT, 'dist/server.js');
|
|
13
15
|
const clientDist = join(ROOT, 'packages/client/dist');
|
|
14
16
|
|
|
15
|
-
//
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Argument parsing
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
16
21
|
const args = process.argv.slice(2);
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
const subcommand = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
23
|
+
const restArgs = subcommand ? args.slice(1) : args;
|
|
24
|
+
|
|
25
|
+
function hasFlag(name) {
|
|
26
|
+
return restArgs.includes(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFlagValue(name) {
|
|
30
|
+
const idx = restArgs.indexOf(name);
|
|
31
|
+
return idx !== -1 ? restArgs[idx + 1] : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const helpFlag = hasFlag('--help') || hasFlag('-h');
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Routing
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
switch (subcommand) {
|
|
41
|
+
case 'server':
|
|
42
|
+
if (helpFlag) { showServerHelp(); process.exit(0); }
|
|
43
|
+
startServer();
|
|
44
|
+
break;
|
|
45
|
+
case 'client':
|
|
46
|
+
if (helpFlag) { showClientHelp(); process.exit(0); }
|
|
47
|
+
startClient();
|
|
48
|
+
break;
|
|
49
|
+
case null:
|
|
50
|
+
if (helpFlag) { showMainHelp(); process.exit(0); }
|
|
51
|
+
startServer(); // backward-compatible default
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.error(`[pi-deck] Unknown command: ${subcommand}\n`);
|
|
55
|
+
showMainHelp();
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
21
58
|
|
|
22
|
-
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Help text
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function showMainHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
pi-deck - Web UI for Pi coding agent
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
pi-deck [options] Start server (default)
|
|
69
|
+
pi-deck server [options] Start the API server
|
|
70
|
+
pi-deck client [options] Start a local client connecting to a remote server
|
|
71
|
+
|
|
72
|
+
Commands:
|
|
73
|
+
server Start the Pi-Deck server (API + WebSocket + serves client UI)
|
|
74
|
+
client Start a local client UI that proxies to a remote server
|
|
75
|
+
|
|
76
|
+
Run 'pi-deck <command> --help' for command-specific options.
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function showServerHelp() {
|
|
23
81
|
console.log(`
|
|
24
|
-
pi-deck - Start the
|
|
82
|
+
pi-deck server - Start the API server
|
|
25
83
|
|
|
26
84
|
Usage:
|
|
27
|
-
pi-deck [options]
|
|
85
|
+
pi-deck server [options]
|
|
28
86
|
|
|
29
87
|
Options:
|
|
30
88
|
--build Build before starting (if not already built)
|
|
31
89
|
--port <n> Override server port (default: 9741)
|
|
32
90
|
-h, --help Show this help message
|
|
33
91
|
|
|
34
|
-
The server
|
|
92
|
+
The server serves the built client UI and exposes the WebSocket API.
|
|
35
93
|
Run 'npm run build' in the project root first, or use --build.
|
|
36
94
|
`);
|
|
37
|
-
process.exit(0);
|
|
38
95
|
}
|
|
39
96
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
function showClientHelp() {
|
|
98
|
+
console.log(`
|
|
99
|
+
pi-deck client - Start a local client connected to a remote server
|
|
43
100
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
101
|
+
Usage:
|
|
102
|
+
pi-deck client --server <url> [options]
|
|
103
|
+
|
|
104
|
+
Options:
|
|
105
|
+
--server <url> URL of the Pi-Deck server (required, e.g. http://remote:9741)
|
|
106
|
+
--port <n> Local port for the client (default: 9740)
|
|
107
|
+
-h, --help Show this help message
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
pi-deck client --server http://my-pod:9741
|
|
111
|
+
pi-deck client --server https://workspace.example.com:9741 --port 8080
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Server mode (default)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function startServer() {
|
|
120
|
+
const buildFlag = hasFlag('--build');
|
|
121
|
+
const portValue = getFlagValue('--port');
|
|
122
|
+
|
|
123
|
+
const serverBuilt = existsSync(bundledServer);
|
|
124
|
+
const clientBuilt = existsSync(clientDist);
|
|
125
|
+
|
|
126
|
+
if (!serverBuilt || !clientBuilt) {
|
|
127
|
+
if (buildFlag) {
|
|
128
|
+
console.log('[pi-deck] Building project...');
|
|
129
|
+
try {
|
|
130
|
+
execSync('npm run build', { cwd: ROOT, stdio: 'inherit' });
|
|
131
|
+
} catch {
|
|
132
|
+
console.error('[pi-deck] Build failed. Fix errors and retry.');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
if (!serverBuilt) console.error(`[pi-deck] Server not built. Missing: ${bundledServer}`);
|
|
137
|
+
if (!clientBuilt) console.error(`[pi-deck] Client not built. Missing: ${clientDist}`);
|
|
138
|
+
console.error('[pi-deck] Run "npm run build" first, or use "pi-deck --build".');
|
|
51
139
|
process.exit(1);
|
|
52
140
|
}
|
|
53
|
-
} else {
|
|
54
|
-
if (!serverBuilt) console.error(`[pi-deck] Server not built. Missing: ${bundledServer}`);
|
|
55
|
-
if (!clientBuilt) console.error(`[pi-deck] Client not built. Missing: ${clientDist}`);
|
|
56
|
-
console.error('[pi-deck] Run "npm run build" first, or use "pi-deck --build".');
|
|
57
|
-
process.exit(1);
|
|
58
141
|
}
|
|
59
|
-
}
|
|
60
142
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
143
|
+
if (portValue) {
|
|
144
|
+
process.env.PORT = portValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Tell the bundled server where the client dist lives
|
|
148
|
+
process.env.PI_DECK_CLIENT_DIST = clientDist;
|
|
149
|
+
|
|
150
|
+
console.log('[pi-deck] Starting server...');
|
|
151
|
+
const child = fork(bundledServer, [], {
|
|
152
|
+
cwd: ROOT,
|
|
153
|
+
stdio: 'inherit',
|
|
154
|
+
env: { ...process.env },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
child.on('exit', (code) => {
|
|
158
|
+
process.exit(code ?? 0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
162
|
+
process.on(sig, () => {
|
|
163
|
+
child.kill(sig);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
64
166
|
}
|
|
65
167
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Client mode — lightweight static server + WebSocket proxy
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function startClient() {
|
|
173
|
+
const serverUrl = getFlagValue('--server');
|
|
174
|
+
const port = parseInt(getFlagValue('--port') || '9740', 10);
|
|
175
|
+
|
|
176
|
+
if (!serverUrl) {
|
|
177
|
+
console.error('[pi-deck] --server <url> is required for client mode.');
|
|
178
|
+
console.error('[pi-deck] Example: pi-deck client --server http://remote:9741');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!existsSync(clientDist)) {
|
|
183
|
+
console.error(`[pi-deck] Client not built. Missing: ${clientDist}`);
|
|
184
|
+
console.error('[pi-deck] Run "npm run build" first, or use "pi-deck server --build".');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const MIME_TYPES = {
|
|
189
|
+
'.html': 'text/html; charset=utf-8',
|
|
190
|
+
'.js': 'application/javascript',
|
|
191
|
+
'.mjs': 'application/javascript',
|
|
192
|
+
'.css': 'text/css',
|
|
193
|
+
'.json': 'application/json',
|
|
194
|
+
'.png': 'image/png',
|
|
195
|
+
'.jpg': 'image/jpeg',
|
|
196
|
+
'.jpeg': 'image/jpeg',
|
|
197
|
+
'.gif': 'image/gif',
|
|
198
|
+
'.svg': 'image/svg+xml',
|
|
199
|
+
'.ico': 'image/x-icon',
|
|
200
|
+
'.woff': 'font/woff',
|
|
201
|
+
'.woff2': 'font/woff2',
|
|
202
|
+
'.ttf': 'font/ttf',
|
|
203
|
+
'.map': 'application/json',
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const parsed = new URL(serverUrl);
|
|
207
|
+
const isHttps = parsed.protocol === 'https:';
|
|
208
|
+
const remoteHost = parsed.hostname;
|
|
209
|
+
const remotePort = parseInt(parsed.port || (isHttps ? '443' : '80'), 10);
|
|
210
|
+
const doRequest = isHttps ? httpsRequest : httpRequest;
|
|
211
|
+
|
|
212
|
+
// ---- HTTP: serve static files, proxy /health to remote ----
|
|
213
|
+
|
|
214
|
+
const httpServer = createServer((req, res) => {
|
|
215
|
+
const urlPath = (req.url || '/').split('?')[0];
|
|
216
|
+
|
|
217
|
+
// Proxy /health to remote server
|
|
218
|
+
if (urlPath === '/health') {
|
|
219
|
+
const proxyReq = doRequest({
|
|
220
|
+
hostname: remoteHost,
|
|
221
|
+
port: remotePort,
|
|
222
|
+
path: req.url,
|
|
223
|
+
method: req.method,
|
|
224
|
+
headers: { ...req.headers, host: `${remoteHost}:${remotePort}` },
|
|
225
|
+
}, (proxyRes) => {
|
|
226
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
227
|
+
proxyRes.pipe(res);
|
|
228
|
+
});
|
|
229
|
+
proxyReq.on('error', () => {
|
|
230
|
+
res.writeHead(502);
|
|
231
|
+
res.end('Bad Gateway — cannot reach server');
|
|
232
|
+
});
|
|
233
|
+
req.pipe(proxyReq);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Serve static files from client dist
|
|
238
|
+
let filePath = join(clientDist, urlPath === '/' ? 'index.html' : urlPath);
|
|
239
|
+
|
|
240
|
+
// SPA fallback — if file doesn't exist, serve index.html
|
|
241
|
+
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
|
|
242
|
+
filePath = join(clientDist, 'index.html');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const content = readFileSync(filePath);
|
|
247
|
+
const ext = extname(filePath);
|
|
248
|
+
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
|
|
249
|
+
res.end(content);
|
|
250
|
+
} catch {
|
|
251
|
+
res.writeHead(404);
|
|
252
|
+
res.end('Not Found');
|
|
253
|
+
}
|
|
85
254
|
});
|
|
255
|
+
|
|
256
|
+
// ---- WebSocket: proxy /ws upgrade to remote server ----
|
|
257
|
+
|
|
258
|
+
httpServer.on('upgrade', (req, clientSocket, head) => {
|
|
259
|
+
if (req.url !== '/ws') {
|
|
260
|
+
clientSocket.destroy();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const proxyReq = doRequest({
|
|
265
|
+
hostname: remoteHost,
|
|
266
|
+
port: remotePort,
|
|
267
|
+
path: '/ws',
|
|
268
|
+
method: 'GET',
|
|
269
|
+
headers: {
|
|
270
|
+
...req.headers,
|
|
271
|
+
host: `${remoteHost}:${remotePort}`,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
proxyReq.on('upgrade', (_proxyRes, proxySocket, proxyHead) => {
|
|
276
|
+
// Forward the raw 101 response headers back to the client
|
|
277
|
+
let response = 'HTTP/1.1 101 Switching Protocols\r\n';
|
|
278
|
+
for (const [key, value] of Object.entries(_proxyRes.headers)) {
|
|
279
|
+
if (value != null) {
|
|
280
|
+
const values = Array.isArray(value) ? value : [value];
|
|
281
|
+
for (const v of values) {
|
|
282
|
+
response += `${key}: ${v}\r\n`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
response += '\r\n';
|
|
287
|
+
|
|
288
|
+
clientSocket.write(response);
|
|
289
|
+
if (proxyHead.length) clientSocket.write(proxyHead);
|
|
290
|
+
if (head.length) proxySocket.write(head);
|
|
291
|
+
|
|
292
|
+
// Bi-directional pipe
|
|
293
|
+
proxySocket.pipe(clientSocket);
|
|
294
|
+
clientSocket.pipe(proxySocket);
|
|
295
|
+
|
|
296
|
+
proxySocket.on('error', () => clientSocket.destroy());
|
|
297
|
+
clientSocket.on('error', () => proxySocket.destroy());
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
proxyReq.on('error', (err) => {
|
|
301
|
+
console.error('[pi-deck] WebSocket proxy error:', err.message);
|
|
302
|
+
clientSocket.destroy();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
proxyReq.end();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ---- Start listening ----
|
|
309
|
+
|
|
310
|
+
httpServer.listen(port, () => {
|
|
311
|
+
console.log('[pi-deck] Client mode');
|
|
312
|
+
console.log(` Local: http://localhost:${port}`);
|
|
313
|
+
console.log(` Server: ${serverUrl}`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
317
|
+
process.on(sig, () => {
|
|
318
|
+
httpServer.close();
|
|
319
|
+
process.exit(0);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
86
322
|
}
|