postforme-terminal 1.0.0 → 1.0.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/dist/index.js +143 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,9 +3,31 @@ 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 findRenderProject() {
|
|
16
|
+
const candidates = [
|
|
17
|
+
process.env.POSTFORME_RENDER_PATH,
|
|
18
|
+
resolve(CWD, 'postforme-render'),
|
|
19
|
+
resolve(CWD, '../postforme-render'),
|
|
20
|
+
resolve(CWD, '../../postforme-render'),
|
|
21
|
+
resolve(homedir(), 'postforme-render'),
|
|
22
|
+
].filter(Boolean);
|
|
23
|
+
for (const p of candidates) {
|
|
24
|
+
if (existsSync(resolve(p, 'package.json')) && existsSync(resolve(p, 'src'))) {
|
|
25
|
+
return p;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const RENDER_PROJECT = findRenderProject();
|
|
9
31
|
const state = {
|
|
10
32
|
process: null,
|
|
11
33
|
buffer: '',
|
|
@@ -28,13 +50,20 @@ function spawnClaude() {
|
|
|
28
50
|
const shell = process.env.SHELL || '/bin/zsh';
|
|
29
51
|
console.log(`[postforme] Starting Claude Code in ${CWD}`);
|
|
30
52
|
try {
|
|
53
|
+
// Build a clean env: strip CLAUDECODE so Claude Code doesn't think it's nested
|
|
54
|
+
const cleanEnv = {};
|
|
55
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
56
|
+
if (v !== undefined && k !== 'CLAUDECODE' && !k.startsWith('CLAUDE_')) {
|
|
57
|
+
cleanEnv[k] = v;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
31
60
|
const proc = pty.spawn(shell, [], {
|
|
32
61
|
name: 'xterm-256color',
|
|
33
62
|
cols: 120,
|
|
34
63
|
rows: 30,
|
|
35
64
|
cwd: CWD,
|
|
36
65
|
env: {
|
|
37
|
-
...
|
|
66
|
+
...cleanEnv,
|
|
38
67
|
TERM: 'xterm-256color',
|
|
39
68
|
HOME: homedir(),
|
|
40
69
|
PATH: `${homedir()}/.local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`,
|
|
@@ -77,9 +106,79 @@ function stopClaude() {
|
|
|
77
106
|
state.buffer = '';
|
|
78
107
|
broadcast({ type: 'status', running: false });
|
|
79
108
|
}
|
|
80
|
-
// ---
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
// --- Remotion Studio Management ---
|
|
110
|
+
let studioProcess = null;
|
|
111
|
+
async function checkPort(port) {
|
|
112
|
+
return new Promise((ok) => {
|
|
113
|
+
const socket = createConnection(port, 'localhost');
|
|
114
|
+
socket.setTimeout(1500);
|
|
115
|
+
socket.on('connect', () => { socket.destroy(); ok(true); });
|
|
116
|
+
socket.on('error', () => ok(false));
|
|
117
|
+
socket.on('timeout', () => { socket.destroy(); ok(false); });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function isStudioRunning() {
|
|
121
|
+
if (studioProcess && studioProcess.exitCode === null)
|
|
122
|
+
return true;
|
|
123
|
+
// Also check if something external is listening on the studio port
|
|
124
|
+
return checkPort(STUDIO_PORT);
|
|
125
|
+
}
|
|
126
|
+
function startStudio(res) {
|
|
127
|
+
if (!RENDER_PROJECT) {
|
|
128
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
129
|
+
res.end(JSON.stringify({ ok: false, error: 'Render project not found. Set POSTFORME_RENDER_PATH or place postforme-render next to your project.' }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (studioProcess && studioProcess.exitCode === null) {
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
134
|
+
res.end(JSON.stringify({ ok: true, message: 'Remotion Studio is already running' }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.log(`[postforme] Starting Remotion Studio on port ${STUDIO_PORT}...`);
|
|
138
|
+
console.log(`[postforme] render project: ${RENDER_PROJECT}`);
|
|
139
|
+
studioProcess = spawn('npx', ['remotion', 'studio', '--port', String(STUDIO_PORT)], {
|
|
140
|
+
cwd: RENDER_PROJECT,
|
|
141
|
+
stdio: 'pipe',
|
|
142
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
143
|
+
detached: false,
|
|
144
|
+
});
|
|
145
|
+
studioProcess.stdout?.on('data', (data) => {
|
|
146
|
+
const line = data.toString().trim();
|
|
147
|
+
if (line)
|
|
148
|
+
console.log(`[remotion-studio] ${line}`);
|
|
149
|
+
});
|
|
150
|
+
studioProcess.stderr?.on('data', (data) => {
|
|
151
|
+
const line = data.toString().trim();
|
|
152
|
+
if (line)
|
|
153
|
+
console.log(`[remotion-studio] ${line}`);
|
|
154
|
+
});
|
|
155
|
+
studioProcess.on('exit', (code) => {
|
|
156
|
+
console.log(`[postforme] Remotion Studio exited with code ${code}`);
|
|
157
|
+
studioProcess = null;
|
|
158
|
+
});
|
|
159
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
160
|
+
res.end(JSON.stringify({ ok: true, message: 'Remotion Studio starting', port: STUDIO_PORT }));
|
|
161
|
+
}
|
|
162
|
+
function stopStudio(res) {
|
|
163
|
+
if (!studioProcess || studioProcess.exitCode !== null) {
|
|
164
|
+
try {
|
|
165
|
+
execSync(`lsof -ti:${STUDIO_PORT} | xargs kill -9 2>/dev/null || true`);
|
|
166
|
+
}
|
|
167
|
+
catch { /* ignore */ }
|
|
168
|
+
studioProcess = null;
|
|
169
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify({ ok: true, message: 'Remotion Studio stopped' }));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log('[postforme] Stopping Remotion Studio...');
|
|
174
|
+
studioProcess.kill('SIGTERM');
|
|
175
|
+
studioProcess = null;
|
|
176
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
177
|
+
res.end(JSON.stringify({ ok: true, message: 'Remotion Studio stopped' }));
|
|
178
|
+
}
|
|
179
|
+
// --- HTTP handler ---
|
|
180
|
+
async function handleHttp(req, res) {
|
|
181
|
+
const origin = req.headers.origin || '';
|
|
83
182
|
const allowed = [
|
|
84
183
|
'http://localhost:5100',
|
|
85
184
|
'http://localhost:3000',
|
|
@@ -88,16 +187,41 @@ function handleHttp(_req, res) {
|
|
|
88
187
|
if (allowed.includes(origin)) {
|
|
89
188
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
90
189
|
}
|
|
91
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
92
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
93
|
-
if (
|
|
190
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
191
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
192
|
+
if (req.method === 'OPTIONS') {
|
|
94
193
|
res.writeHead(204);
|
|
95
194
|
res.end();
|
|
96
195
|
return;
|
|
97
196
|
}
|
|
98
|
-
|
|
197
|
+
// GET /status — full service status (matches LocalServicesGuard expectations)
|
|
198
|
+
if (req.method === 'GET' && req.url === '/status') {
|
|
199
|
+
const studioRunning = await isStudioRunning();
|
|
200
|
+
// Clean up dead studio process reference
|
|
201
|
+
if (studioProcess && studioProcess.exitCode !== null) {
|
|
202
|
+
studioProcess = null;
|
|
203
|
+
}
|
|
99
204
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
100
|
-
res.end(JSON.stringify({
|
|
205
|
+
res.end(JSON.stringify({
|
|
206
|
+
terminalServer: true,
|
|
207
|
+
claudeCode: state.process !== null,
|
|
208
|
+
remotionStudio: studioRunning,
|
|
209
|
+
renderProject: RENDER_PROJECT || null,
|
|
210
|
+
ports: {
|
|
211
|
+
terminalServer: PORT,
|
|
212
|
+
remotionStudio: STUDIO_PORT,
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// POST /services/studio/start
|
|
218
|
+
if (req.method === 'POST' && req.url === '/services/studio/start') {
|
|
219
|
+
startStudio(res);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// POST /services/studio/stop
|
|
223
|
+
if (req.method === 'POST' && req.url === '/services/studio/stop') {
|
|
224
|
+
stopStudio(res);
|
|
101
225
|
return;
|
|
102
226
|
}
|
|
103
227
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
@@ -154,6 +278,12 @@ httpServer.listen(PORT, () => {
|
|
|
154
278
|
console.log('');
|
|
155
279
|
console.log(' PostForMe Terminal Server running');
|
|
156
280
|
console.log(` → ws://localhost:${PORT}`);
|
|
281
|
+
if (RENDER_PROJECT) {
|
|
282
|
+
console.log(` → Remotion render project: ${RENDER_PROJECT}`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
console.log(' → Remotion: not found (set POSTFORME_RENDER_PATH or place postforme-render nearby)');
|
|
286
|
+
}
|
|
157
287
|
console.log('');
|
|
158
288
|
console.log(' Open postforme.ca and click the Terminal button.');
|
|
159
289
|
console.log(' Press Ctrl+C to stop.');
|
|
@@ -166,6 +296,10 @@ function shutdown(signal) {
|
|
|
166
296
|
state.process.kill();
|
|
167
297
|
state.process = null;
|
|
168
298
|
}
|
|
299
|
+
if (studioProcess && studioProcess.exitCode === null) {
|
|
300
|
+
studioProcess.kill('SIGTERM');
|
|
301
|
+
studioProcess = null;
|
|
302
|
+
}
|
|
169
303
|
for (const client of state.clients) {
|
|
170
304
|
client.close();
|
|
171
305
|
}
|