2026-03-30 14:23:28 +00:00
|
|
|
|
'use strict';
|
|
|
|
|
|
const express = require('express');
|
|
|
|
|
|
const { WebSocketServer } = require('ws');
|
|
|
|
|
|
const pty = require('node-pty');
|
|
|
|
|
|
const http = require('http');
|
2026-03-30 14:45:25 +00:00
|
|
|
|
const https = require('https');
|
|
|
|
|
|
const fs = require('fs');
|
2026-03-30 14:23:28 +00:00
|
|
|
|
const path = require('path');
|
2026-03-30 14:45:25 +00:00
|
|
|
|
const os = require('os');
|
2026-03-30 14:23:28 +00:00
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
|
const PORT = process.env.PORT || 3333;
|
|
|
|
|
|
|
2026-03-30 15:42:16 +00:00
|
|
|
|
// セッションログ保存ディレクトリ
|
|
|
|
|
|
const SESSIONS_DIR = path.join(os.homedir(), 'posimai-dev-sessions');
|
|
|
|
|
|
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
|
|
|
|
|
|
|
|
|
|
app.use(express.json());
|
2026-03-30 14:23:28 +00:00
|
|
|
|
app.use(express.static(path.join(__dirname)));
|
|
|
|
|
|
|
2026-03-30 15:42:16 +00:00
|
|
|
|
// セッション一覧 API
|
|
|
|
|
|
app.get('/api/sessions', (req, res) => {
|
|
|
|
|
|
const files = fs.readdirSync(SESSIONS_DIR)
|
|
|
|
|
|
.filter((f) => f.endsWith('.log'))
|
|
|
|
|
|
.map((f) => {
|
|
|
|
|
|
const stat = fs.statSync(path.join(SESSIONS_DIR, f));
|
|
|
|
|
|
return { id: f.replace('.log', ''), size: stat.size, mtime: stat.mtime };
|
|
|
|
|
|
})
|
|
|
|
|
|
.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
|
|
|
|
|
|
res.json(files);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// セッション内容 API
|
|
|
|
|
|
app.get('/api/sessions/:id', (req, res) => {
|
|
|
|
|
|
const file = path.join(SESSIONS_DIR, req.params.id + '.log');
|
|
|
|
|
|
if (!fs.existsSync(file)) return res.status(404).json({ error: 'not found' });
|
|
|
|
|
|
res.type('text/plain').send(fs.readFileSync(file, 'utf8'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-30 22:48:01 +00:00
|
|
|
|
// ── ヘルス & メトリクス API (/api/health) ──────────────────────
|
|
|
|
|
|
// Atlas など外部から参照される。CORS ヘッダーを付与して Vercel 上の Atlas からも取得可能にする
|
|
|
|
|
|
app.get('/api/health', (req, res) => {
|
|
|
|
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
|
|
|
|
|
|
|
|
const mem = os.freemem();
|
|
|
|
|
|
const total = os.totalmem();
|
|
|
|
|
|
const cpus = os.cpus();
|
|
|
|
|
|
|
|
|
|
|
|
// CPU 使用率: 全コアの平均(起動時 idle から差し引く簡易計算)
|
|
|
|
|
|
const cpuUsage = cpus.reduce((sum, c) => {
|
|
|
|
|
|
const t = Object.values(c.times).reduce((a, b) => a + b, 0);
|
|
|
|
|
|
return sum + ((t - c.times.idle) / t) * 100;
|
|
|
|
|
|
}, 0) / cpus.length;
|
|
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
hostname: os.hostname(),
|
|
|
|
|
|
uptime_s: Math.floor(os.uptime()),
|
|
|
|
|
|
cpu_pct: Math.round(cpuUsage),
|
|
|
|
|
|
mem_used_mb: Math.round((total - mem) / 1024 / 1024),
|
|
|
|
|
|
mem_total_mb: Math.round(total / 1024 / 1024),
|
|
|
|
|
|
active_sessions: wss.clients ? wss.clients.size : 0,
|
|
|
|
|
|
node_version: process.version,
|
|
|
|
|
|
platform: os.platform(),
|
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-30 15:42:16 +00:00
|
|
|
|
// Tailscale証明書を自動検出
|
2026-03-30 14:45:25 +00:00
|
|
|
|
function findCert() {
|
|
|
|
|
|
const home = os.homedir();
|
|
|
|
|
|
const crt = fs.readdirSync(home).find((f) => f.endsWith('.crt'));
|
|
|
|
|
|
if (!crt) return null;
|
|
|
|
|
|
const key = crt.replace('.crt', '.key');
|
|
|
|
|
|
if (!fs.existsSync(path.join(home, key))) return null;
|
|
|
|
|
|
return { cert: fs.readFileSync(path.join(home, crt)), key: fs.readFileSync(path.join(home, key)) };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tlsOpts = findCert();
|
|
|
|
|
|
const server = tlsOpts ? https.createServer(tlsOpts, app) : http.createServer(app);
|
|
|
|
|
|
const proto = tlsOpts ? 'https' : 'http';
|
|
|
|
|
|
|
2026-03-30 14:23:28 +00:00
|
|
|
|
const wss = new WebSocketServer({ server, path: '/terminal' });
|
|
|
|
|
|
|
|
|
|
|
|
wss.on('connection', (ws) => {
|
2026-03-30 15:42:16 +00:00
|
|
|
|
// セッションID・ログファイル作成
|
|
|
|
|
|
const sessionId = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
|
|
|
|
const logPath = path.join(SESSIONS_DIR, `${sessionId}.log`);
|
|
|
|
|
|
const logStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
|
|
|
|
logStream.write(`=== posimai-dev session ${sessionId} ===\n`);
|
|
|
|
|
|
|
|
|
|
|
|
// セッションID をブラウザに通知
|
|
|
|
|
|
ws.send(JSON.stringify({ type: 'session', id: sessionId }));
|
|
|
|
|
|
|
2026-03-30 14:23:28 +00:00
|
|
|
|
const shell = process.env.SHELL || '/bin/bash';
|
|
|
|
|
|
const ptyProc = pty.spawn(shell, [], {
|
|
|
|
|
|
name: 'xterm-256color',
|
|
|
|
|
|
cols: 80,
|
|
|
|
|
|
rows: 24,
|
2026-03-30 15:42:16 +00:00
|
|
|
|
cwd: path.join(os.homedir(), 'posimai-project'),
|
|
|
|
|
|
env: { ...process.env, PATH: `${os.homedir()}/.npm-global/bin:${process.env.PATH}` }
|
2026-03-30 14:23:28 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-30 15:42:16 +00:00
|
|
|
|
// 起動時に posimai-project へ自動移動(cwd で指定済みだが source bashrc も通す)
|
|
|
|
|
|
setTimeout(() => ptyProc.write(`source ~/.bashrc\n`), 300);
|
|
|
|
|
|
|
2026-03-30 14:23:28 +00:00
|
|
|
|
ptyProc.onData((data) => {
|
2026-03-30 15:42:16 +00:00
|
|
|
|
logStream.write(data);
|
|
|
|
|
|
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'output', data }));
|
2026-03-30 14:23:28 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
ws.on('message', (raw) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const msg = JSON.parse(raw.toString());
|
2026-03-30 15:42:16 +00:00
|
|
|
|
if (msg.type === 'input') {
|
|
|
|
|
|
logStream.write(msg.data);
|
|
|
|
|
|
ptyProc.write(msg.data);
|
|
|
|
|
|
}
|
2026-03-30 14:23:28 +00:00
|
|
|
|
if (msg.type === 'resize') ptyProc.resize(Number(msg.cols), Number(msg.rows));
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-30 15:42:16 +00:00
|
|
|
|
ws.on('close', () => {
|
|
|
|
|
|
logStream.end(`\n=== session end ===\n`);
|
|
|
|
|
|
try { ptyProc.kill(); } catch {}
|
|
|
|
|
|
});
|
2026-03-30 14:23:28 +00:00
|
|
|
|
ptyProc.onExit(() => { try { ws.close(); } catch {} });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
server.listen(PORT, '0.0.0.0', () => {
|
2026-03-30 14:45:25 +00:00
|
|
|
|
console.log(`posimai-dev running on ${proto}://0.0.0.0:${PORT}`);
|
2026-03-30 14:23:28 +00:00
|
|
|
|
});
|