postforme-terminal 1.0.1 → 1.0.3

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 (2) hide show
  1. package/dist/index.js +163 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,9 +3,59 @@ import { WebSocketServer, WebSocket } from 'ws';
3
3
  import * as pty from 'node-pty';
4
4
  import { homedir } from 'os';
5
5
  import { createServer } from 'http';
6
+ import { spawn, execSync } from 'child_process';
7
+ import { createConnection } from 'net';
8
+ import { existsSync } from 'fs';
9
+ import { resolve } from 'path';
6
10
  const PORT = parseInt(process.env.TERMINAL_PORT || '5101', 10);
11
+ const STUDIO_PORT = parseInt(process.env.REMOTION_STUDIO_PORT || '3100', 10);
7
12
  const CWD = process.cwd();
8
13
  const MAX_BUFFER = 100_000;
14
+ // --- Locate render project ---
15
+ function isRenderProject(dir) {
16
+ return existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'src'));
17
+ }
18
+ function findRenderProject() {
19
+ // 1. Explicit env var (highest priority)
20
+ if (process.env.POSTFORME_RENDER_PATH && isRenderProject(process.env.POSTFORME_RENDER_PATH)) {
21
+ return process.env.POSTFORME_RENDER_PATH;
22
+ }
23
+ // 2. Walk up from CWD checking siblings and children at each level
24
+ let dir = CWD;
25
+ for (let i = 0; i < 5; i++) {
26
+ // Check as sibling
27
+ const sibling = resolve(dir, 'postforme-render');
28
+ if (isRenderProject(sibling))
29
+ return sibling;
30
+ // Check inside a postforme subfolder (e.g. user ran from ~/Projects, render is in ~/Projects/postforme/postforme-render)
31
+ const nested = resolve(dir, 'postforme', 'postforme-render');
32
+ if (isRenderProject(nested))
33
+ return nested;
34
+ const parent = resolve(dir, '..');
35
+ if (parent === dir)
36
+ break; // hit root
37
+ dir = parent;
38
+ }
39
+ // 3. Common locations
40
+ const home = homedir();
41
+ const commonPaths = [
42
+ resolve(home, 'postforme-render'),
43
+ resolve(home, 'postforme', 'postforme-render'),
44
+ resolve(home, 'Desktop', 'postforme', 'postforme-render'),
45
+ resolve(home, 'Desktop', 'Projects', 'postforme', 'postforme-render'),
46
+ resolve(home, 'Documents', 'postforme', 'postforme-render'),
47
+ resolve(home, 'projects', 'postforme', 'postforme-render'),
48
+ resolve(home, 'Projects', 'postforme', 'postforme-render'),
49
+ resolve(home, 'dev', 'postforme', 'postforme-render'),
50
+ resolve(home, '.postforme', 'render'),
51
+ ];
52
+ for (const p of commonPaths) {
53
+ if (isRenderProject(p))
54
+ return p;
55
+ }
56
+ return null;
57
+ }
58
+ const RENDER_PROJECT = findRenderProject();
9
59
  const state = {
10
60
  process: null,
11
61
  buffer: '',
@@ -84,9 +134,79 @@ function stopClaude() {
84
134
  state.buffer = '';
85
135
  broadcast({ type: 'status', running: false });
86
136
  }
87
- // --- HTTP handler (health check + CORS) ---
88
- function handleHttp(_req, res) {
89
- const origin = _req.headers.origin || '';
137
+ // --- Remotion Studio Management ---
138
+ let studioProcess = null;
139
+ async function checkPort(port) {
140
+ return new Promise((ok) => {
141
+ const socket = createConnection(port, 'localhost');
142
+ socket.setTimeout(1500);
143
+ socket.on('connect', () => { socket.destroy(); ok(true); });
144
+ socket.on('error', () => ok(false));
145
+ socket.on('timeout', () => { socket.destroy(); ok(false); });
146
+ });
147
+ }
148
+ async function isStudioRunning() {
149
+ if (studioProcess && studioProcess.exitCode === null)
150
+ return true;
151
+ // Also check if something external is listening on the studio port
152
+ return checkPort(STUDIO_PORT);
153
+ }
154
+ function startStudio(res) {
155
+ if (!RENDER_PROJECT) {
156
+ res.writeHead(404, { 'Content-Type': 'application/json' });
157
+ res.end(JSON.stringify({ ok: false, error: 'Render project not found. Set POSTFORME_RENDER_PATH or place postforme-render next to your project.' }));
158
+ return;
159
+ }
160
+ if (studioProcess && studioProcess.exitCode === null) {
161
+ res.writeHead(200, { 'Content-Type': 'application/json' });
162
+ res.end(JSON.stringify({ ok: true, message: 'Remotion Studio is already running' }));
163
+ return;
164
+ }
165
+ console.log(`[postforme] Starting Remotion Studio on port ${STUDIO_PORT}...`);
166
+ console.log(`[postforme] render project: ${RENDER_PROJECT}`);
167
+ studioProcess = spawn('npx', ['remotion', 'studio', '--port', String(STUDIO_PORT)], {
168
+ cwd: RENDER_PROJECT,
169
+ stdio: 'pipe',
170
+ env: { ...process.env, FORCE_COLOR: '0' },
171
+ detached: false,
172
+ });
173
+ studioProcess.stdout?.on('data', (data) => {
174
+ const line = data.toString().trim();
175
+ if (line)
176
+ console.log(`[remotion-studio] ${line}`);
177
+ });
178
+ studioProcess.stderr?.on('data', (data) => {
179
+ const line = data.toString().trim();
180
+ if (line)
181
+ console.log(`[remotion-studio] ${line}`);
182
+ });
183
+ studioProcess.on('exit', (code) => {
184
+ console.log(`[postforme] Remotion Studio exited with code ${code}`);
185
+ studioProcess = null;
186
+ });
187
+ res.writeHead(200, { 'Content-Type': 'application/json' });
188
+ res.end(JSON.stringify({ ok: true, message: 'Remotion Studio starting', port: STUDIO_PORT }));
189
+ }
190
+ function stopStudio(res) {
191
+ if (!studioProcess || studioProcess.exitCode !== null) {
192
+ try {
193
+ execSync(`lsof -ti:${STUDIO_PORT} | xargs kill -9 2>/dev/null || true`);
194
+ }
195
+ catch { /* ignore */ }
196
+ studioProcess = null;
197
+ res.writeHead(200, { 'Content-Type': 'application/json' });
198
+ res.end(JSON.stringify({ ok: true, message: 'Remotion Studio stopped' }));
199
+ return;
200
+ }
201
+ console.log('[postforme] Stopping Remotion Studio...');
202
+ studioProcess.kill('SIGTERM');
203
+ studioProcess = null;
204
+ res.writeHead(200, { 'Content-Type': 'application/json' });
205
+ res.end(JSON.stringify({ ok: true, message: 'Remotion Studio stopped' }));
206
+ }
207
+ // --- HTTP handler ---
208
+ async function handleHttp(req, res) {
209
+ const origin = req.headers.origin || '';
90
210
  const allowed = [
91
211
  'http://localhost:5100',
92
212
  'http://localhost:3000',
@@ -95,16 +215,41 @@ function handleHttp(_req, res) {
95
215
  if (allowed.includes(origin)) {
96
216
  res.setHeader('Access-Control-Allow-Origin', origin);
97
217
  }
98
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
99
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
100
- if (_req.method === 'OPTIONS') {
218
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
219
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
220
+ if (req.method === 'OPTIONS') {
101
221
  res.writeHead(204);
102
222
  res.end();
103
223
  return;
104
224
  }
105
- if (_req.url === '/status') {
225
+ // GET /status — full service status (matches LocalServicesGuard expectations)
226
+ if (req.method === 'GET' && req.url === '/status') {
227
+ const studioRunning = await isStudioRunning();
228
+ // Clean up dead studio process reference
229
+ if (studioProcess && studioProcess.exitCode !== null) {
230
+ studioProcess = null;
231
+ }
106
232
  res.writeHead(200, { 'Content-Type': 'application/json' });
107
- res.end(JSON.stringify({ ok: true, claudeCode: state.process !== null }));
233
+ res.end(JSON.stringify({
234
+ terminalServer: true,
235
+ claudeCode: state.process !== null,
236
+ remotionStudio: studioRunning,
237
+ renderProject: RENDER_PROJECT || null,
238
+ ports: {
239
+ terminalServer: PORT,
240
+ remotionStudio: STUDIO_PORT,
241
+ },
242
+ }));
243
+ return;
244
+ }
245
+ // POST /services/studio/start
246
+ if (req.method === 'POST' && req.url === '/services/studio/start') {
247
+ startStudio(res);
248
+ return;
249
+ }
250
+ // POST /services/studio/stop
251
+ if (req.method === 'POST' && req.url === '/services/studio/stop') {
252
+ stopStudio(res);
108
253
  return;
109
254
  }
110
255
  res.writeHead(200, { 'Content-Type': 'text/plain' });
@@ -161,6 +306,12 @@ httpServer.listen(PORT, () => {
161
306
  console.log('');
162
307
  console.log(' PostForMe Terminal Server running');
163
308
  console.log(` → ws://localhost:${PORT}`);
309
+ if (RENDER_PROJECT) {
310
+ console.log(` → Remotion render project: ${RENDER_PROJECT}`);
311
+ }
312
+ else {
313
+ console.log(' → Remotion: not found (set POSTFORME_RENDER_PATH or place postforme-render nearby)');
314
+ }
164
315
  console.log('');
165
316
  console.log(' Open postforme.ca and click the Terminal button.');
166
317
  console.log(' Press Ctrl+C to stop.');
@@ -173,6 +324,10 @@ function shutdown(signal) {
173
324
  state.process.kill();
174
325
  state.process = null;
175
326
  }
327
+ if (studioProcess && studioProcess.exitCode === null) {
328
+ studioProcess.kill('SIGTERM');
329
+ studioProcess = null;
330
+ }
176
331
  for (const client of state.clients) {
177
332
  client.close();
178
333
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postforme-terminal",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "PostForMe terminal server — connects your browser to Claude Code",
5
5
  "type": "module",
6
6
  "bin": {