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.
- package/dist/index.js +163 -8
- 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
|
-
// ---
|
|
88
|
-
|
|
89
|
-
|
|
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 (
|
|
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
|
-
|
|
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({
|
|
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
|
}
|