pi-deck 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.
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 build # builds shared server → client → bundles server with esbuild
89
- npm pack # creates a .tgz to inspect before publishing
90
- npm publish # publish to the npm registry
88
+ npm run publish:npm patch # 0.1.00.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,313 @@ 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
- // Parse CLI flags
17
+ // ---------------------------------------------------------------------------
18
+ // Argument parsing
19
+ // ---------------------------------------------------------------------------
20
+
16
21
  const args = process.argv.slice(2);
17
- const helpFlag = args.includes('--help') || args.includes('-h');
18
- const buildFlag = args.includes('--build');
19
- const portFlag = args.indexOf('--port');
20
- const portValue = portFlag !== -1 ? args[portFlag + 1] : undefined;
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
- if (helpFlag) {
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 Pi-Deck server
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 will serve the built client UI and expose the WebSocket API.
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
- // Check if build exists, offer to build if not
41
- const serverBuilt = existsSync(bundledServer);
42
- const clientBuilt = existsSync(clientDist);
97
+ function showClientHelp() {
98
+ console.log(`
99
+ pi-deck client - Start a local client connected to a remote server
100
+
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
+ // ---------------------------------------------------------------------------
43
118
 
44
- if (!serverBuilt || !clientBuilt) {
45
- if (buildFlag) {
46
- console.log('[pi-deck] Building project...');
47
- try {
48
- execSync('npm run build', { cwd: ROOT, stdio: 'inherit' });
49
- } catch {
50
- console.error('[pi-deck] Build failed. Fix errors and retry.');
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
- // Set port via env if provided
62
- if (portValue) {
63
- process.env.PORT = portValue;
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
+ // Pass version so the bundled server doesn't need to find package.json
151
+ const pkgJson = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
152
+ process.env.PI_DECK_VERSION = pkgJson.version;
153
+
154
+ console.log('[pi-deck] Starting server...');
155
+ const child = fork(bundledServer, [], {
156
+ cwd: ROOT,
157
+ stdio: 'inherit',
158
+ env: { ...process.env },
159
+ });
160
+
161
+ child.on('exit', (code) => {
162
+ process.exit(code ?? 0);
163
+ });
164
+
165
+ for (const sig of ['SIGINT', 'SIGTERM']) {
166
+ process.on(sig, () => {
167
+ child.kill(sig);
168
+ });
169
+ }
64
170
  }
65
171
 
66
- // Tell the bundled server where the client dist lives
67
- process.env.PI_DECK_CLIENT_DIST = clientDist;
68
-
69
- // Start the server
70
- console.log('[pi-deck] Starting Pi-Deck server...');
71
- const child = fork(bundledServer, [], {
72
- cwd: ROOT,
73
- stdio: 'inherit',
74
- env: { ...process.env },
75
- });
76
-
77
- child.on('exit', (code) => {
78
- process.exit(code ?? 0);
79
- });
80
-
81
- // Forward signals for clean shutdown
82
- for (const sig of ['SIGINT', 'SIGTERM']) {
83
- process.on(sig, () => {
84
- child.kill(sig);
172
+ // ---------------------------------------------------------------------------
173
+ // Client mode — lightweight static server + WebSocket proxy
174
+ // ---------------------------------------------------------------------------
175
+
176
+ function startClient() {
177
+ const serverUrl = getFlagValue('--server');
178
+ const port = parseInt(getFlagValue('--port') || '9740', 10);
179
+
180
+ if (!serverUrl) {
181
+ console.error('[pi-deck] --server <url> is required for client mode.');
182
+ console.error('[pi-deck] Example: pi-deck client --server http://remote:9741');
183
+ process.exit(1);
184
+ }
185
+
186
+ if (!existsSync(clientDist)) {
187
+ console.error(`[pi-deck] Client not built. Missing: ${clientDist}`);
188
+ console.error('[pi-deck] Run "npm run build" first, or use "pi-deck server --build".');
189
+ process.exit(1);
190
+ }
191
+
192
+ const MIME_TYPES = {
193
+ '.html': 'text/html; charset=utf-8',
194
+ '.js': 'application/javascript',
195
+ '.mjs': 'application/javascript',
196
+ '.css': 'text/css',
197
+ '.json': 'application/json',
198
+ '.png': 'image/png',
199
+ '.jpg': 'image/jpeg',
200
+ '.jpeg': 'image/jpeg',
201
+ '.gif': 'image/gif',
202
+ '.svg': 'image/svg+xml',
203
+ '.ico': 'image/x-icon',
204
+ '.woff': 'font/woff',
205
+ '.woff2': 'font/woff2',
206
+ '.ttf': 'font/ttf',
207
+ '.map': 'application/json',
208
+ };
209
+
210
+ const parsed = new URL(serverUrl);
211
+ const isHttps = parsed.protocol === 'https:';
212
+ const remoteHost = parsed.hostname;
213
+ const remotePort = parseInt(parsed.port || (isHttps ? '443' : '80'), 10);
214
+ const doRequest = isHttps ? httpsRequest : httpRequest;
215
+
216
+ // ---- HTTP: serve static files, proxy /health to remote ----
217
+
218
+ const httpServer = createServer((req, res) => {
219
+ const urlPath = (req.url || '/').split('?')[0];
220
+
221
+ // Proxy /health to remote server
222
+ if (urlPath === '/health') {
223
+ const proxyReq = doRequest({
224
+ hostname: remoteHost,
225
+ port: remotePort,
226
+ path: req.url,
227
+ method: req.method,
228
+ headers: { ...req.headers, host: `${remoteHost}:${remotePort}` },
229
+ }, (proxyRes) => {
230
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
231
+ proxyRes.pipe(res);
232
+ });
233
+ proxyReq.on('error', () => {
234
+ res.writeHead(502);
235
+ res.end('Bad Gateway — cannot reach server');
236
+ });
237
+ req.pipe(proxyReq);
238
+ return;
239
+ }
240
+
241
+ // Serve static files from client dist
242
+ let filePath = join(clientDist, urlPath === '/' ? 'index.html' : urlPath);
243
+
244
+ // SPA fallback — if file doesn't exist, serve index.html
245
+ if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
246
+ filePath = join(clientDist, 'index.html');
247
+ }
248
+
249
+ try {
250
+ const content = readFileSync(filePath);
251
+ const ext = extname(filePath);
252
+ res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
253
+ res.end(content);
254
+ } catch {
255
+ res.writeHead(404);
256
+ res.end('Not Found');
257
+ }
85
258
  });
259
+
260
+ // ---- WebSocket: proxy /ws upgrade to remote server ----
261
+
262
+ httpServer.on('upgrade', (req, clientSocket, head) => {
263
+ if (req.url !== '/ws') {
264
+ clientSocket.destroy();
265
+ return;
266
+ }
267
+
268
+ const proxyReq = doRequest({
269
+ hostname: remoteHost,
270
+ port: remotePort,
271
+ path: '/ws',
272
+ method: 'GET',
273
+ headers: {
274
+ ...req.headers,
275
+ host: `${remoteHost}:${remotePort}`,
276
+ },
277
+ });
278
+
279
+ proxyReq.on('upgrade', (_proxyRes, proxySocket, proxyHead) => {
280
+ // Forward the raw 101 response headers back to the client
281
+ let response = 'HTTP/1.1 101 Switching Protocols\r\n';
282
+ for (const [key, value] of Object.entries(_proxyRes.headers)) {
283
+ if (value != null) {
284
+ const values = Array.isArray(value) ? value : [value];
285
+ for (const v of values) {
286
+ response += `${key}: ${v}\r\n`;
287
+ }
288
+ }
289
+ }
290
+ response += '\r\n';
291
+
292
+ clientSocket.write(response);
293
+ if (proxyHead.length) clientSocket.write(proxyHead);
294
+ if (head.length) proxySocket.write(head);
295
+
296
+ // Bi-directional pipe
297
+ proxySocket.pipe(clientSocket);
298
+ clientSocket.pipe(proxySocket);
299
+
300
+ proxySocket.on('error', () => clientSocket.destroy());
301
+ clientSocket.on('error', () => proxySocket.destroy());
302
+ });
303
+
304
+ proxyReq.on('error', (err) => {
305
+ console.error('[pi-deck] WebSocket proxy error:', err.message);
306
+ clientSocket.destroy();
307
+ });
308
+
309
+ proxyReq.end();
310
+ });
311
+
312
+ // ---- Start listening ----
313
+
314
+ httpServer.listen(port, () => {
315
+ console.log('[pi-deck] Client mode');
316
+ console.log(` Local: http://localhost:${port}`);
317
+ console.log(` Server: ${serverUrl}`);
318
+ });
319
+
320
+ for (const sig of ['SIGINT', 'SIGTERM']) {
321
+ process.on(sig, () => {
322
+ httpServer.close();
323
+ process.exit(0);
324
+ });
325
+ }
86
326
  }