mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
Merge pull request #103 from Ark0N/beta/session-detach
feat(web): session detach/undock + beta instance isolation (port 5000)
This commit is contained in:
@@ -17,8 +17,9 @@ import { writeHooksConfig } from '../../hooks-config.js';
|
||||
import { CASES_DIR, SETTINGS_PATH, validatePathWithinBase, parseBody, readJsonConfig } from '../route-helpers.js';
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
import type { EventPort, ConfigPort } from '../ports/index.js';
|
||||
import { dataPath, getDataDir } from '../../config/instance.js';
|
||||
|
||||
const LINKED_CASES_FILE = join(homedir(), '.codeman', 'linked-cases.json');
|
||||
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
||||
const SAFE_CASE_NAME = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
/** Read and parse linked-cases.json, returning empty object on missing/invalid file. */
|
||||
@@ -151,7 +152,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
||||
// Save the linked case
|
||||
linkedCases[name] = expandedPath;
|
||||
try {
|
||||
const codemanDir = join(homedir(), '.codeman');
|
||||
const codemanDir = getDataDir();
|
||||
if (!existsSync(codemanDir)) {
|
||||
mkdirSync(codemanDir, { recursive: true });
|
||||
}
|
||||
@@ -206,7 +207,7 @@ export function registerCaseRoutes(app: FastifyInstance, ctx: EventPort & Config
|
||||
const { order } = parseBody(CaseOrderSchema, req.body, 'Invalid order data');
|
||||
|
||||
try {
|
||||
const dir = join(homedir(), '.codeman');
|
||||
const dir = getDataDir();
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
@@ -54,9 +54,10 @@ import { MAX_CONCURRENT_SESSIONS } from '../../config/map-limits.js';
|
||||
import { RunSummaryTracker } from '../../run-summary.js';
|
||||
|
||||
import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
|
||||
import { dataPath } from '../../config/instance.js';
|
||||
|
||||
// Path to linked-cases registry (same file used by case-routes resolveCasePath)
|
||||
const LINKED_CASES_FILE = join(homedir(), '.codeman', 'linked-cases.json');
|
||||
const LINKED_CASES_FILE = dataPath('linked-cases.json');
|
||||
|
||||
// Pre-compiled regex for terminal buffer cleaning (avoids per-request compilation)
|
||||
// eslint-disable-next-line no-control-regex
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { homedir, totalmem, freemem, loadavg, cpus } from 'node:os';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { totalmem, freemem, loadavg, cpus } from 'node:os';
|
||||
import { execSync, spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { dataPath } from '../../config/instance.js';
|
||||
import { ApiErrorCode, createErrorResponse, getErrorMessage, type NiceConfig } from '../../types.js';
|
||||
import {
|
||||
ConfigUpdateSchema,
|
||||
@@ -41,7 +43,7 @@ import { AUTH_SESSION_TTL_MS } from '../../config/auth-config.js';
|
||||
// Maximum screenshot upload size (10MB)
|
||||
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
|
||||
// Screenshots directory
|
||||
const SCREENSHOTS_DIR = join(homedir(), '.codeman', 'screenshots');
|
||||
const SCREENSHOTS_DIR = dataPath('screenshots');
|
||||
|
||||
/** Cached CPU count — doesn't change at runtime */
|
||||
const CPU_COUNT = cpus().length;
|
||||
@@ -92,12 +94,24 @@ function getSystemStats(): {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL the spanning browser window should open, pinned to localhost.
|
||||
* Takes only a digits-only port from the (untrusted) Host header so nothing
|
||||
* attacker-controllable reaches the launched browser; falls back to the default
|
||||
* port when the header is absent/odd. Exported for unit testing.
|
||||
*/
|
||||
export function resolveSpanUrl(hostHeader: string | undefined, fallbackPort = '3000'): string {
|
||||
const hostPort = String(hostHeader ?? '').split(':')[1] ?? '';
|
||||
const port = /^\d+$/.test(hostPort) ? hostPort : fallbackPort;
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
export function registerSystemRoutes(
|
||||
app: FastifyInstance,
|
||||
ctx: SessionPort & EventPort & ConfigPort & InfraPort & AuthPort
|
||||
): void {
|
||||
const windowStatesPath = join(homedir(), '.codeman', 'subagent-window-states.json');
|
||||
const parentMapPath = join(homedir(), '.codeman', 'subagent-parents.json');
|
||||
const windowStatesPath = dataPath('subagent-window-states.json');
|
||||
const parentMapPath = dataPath('subagent-parents.json');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// System Status & Health
|
||||
@@ -239,6 +253,40 @@ export function registerSystemRoutes(
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Multi-monitor: span Codeman across all displays
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
// Spawn scripts/span-codeman.sh, which opens a fresh, maximized browser --app
|
||||
// window sized to the union of all displays — so in-page floating session
|
||||
// panels can be dragged across the physical monitor seam. macOS only; needs
|
||||
// the one-time "Displays have separate Spaces" OFF prerequisite (see script).
|
||||
app.post('/api/system/span-displays', async (req, reply) => {
|
||||
// macOS only: the launcher uses osascript + Finder desktop bounds and Chrome
|
||||
// --app geometry flags. Fail clearly elsewhere instead of spawning a bash
|
||||
// that errors out invisibly (the toast would otherwise lie "Opening…").
|
||||
if (process.platform !== 'darwin') {
|
||||
return reply
|
||||
.code(400)
|
||||
.send(createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Multi-monitor spanning is only supported on macOS.'));
|
||||
}
|
||||
// Resolve the bundled launcher relative to this module (works from src/ and dist/).
|
||||
const scriptPath = join(dirname(fileURLToPath(import.meta.url)), '../../../scripts/span-codeman.sh');
|
||||
if (!existsSync(scriptPath)) {
|
||||
return reply.code(500).send(createErrorResponse(ApiErrorCode.INTERNAL_ERROR, 'span-codeman.sh not found'));
|
||||
}
|
||||
// Point the spanning window at THIS server (localhost + sanitized port).
|
||||
const url = resolveSpanUrl(req.headers.host);
|
||||
try {
|
||||
const child = spawn('bash', [scriptPath, url], { detached: true, stdio: 'ignore' });
|
||||
child.on('error', (err) => app.log.error({ err }, 'span-displays launch failed'));
|
||||
child.unref();
|
||||
return { success: true, url };
|
||||
} catch (err) {
|
||||
return reply.code(500).send(createErrorResponse(ApiErrorCode.INTERNAL_ERROR, getErrorMessage(err)));
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// CLI Integrations (OpenCode)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user