Skip to content

feat(backend): add unified backend server package#49

Merged
maystudios merged 1 commit intomainfrom
worktree-agent-a53b36fa
Mar 2, 2026
Merged

feat(backend): add unified backend server package#49
maystudios merged 1 commit intomainfrom
worktree-agent-a53b36fa

Conversation

@maystudios
Copy link
Owner

Summary

  • Add a unified persistent backend service for MAXSIM that consolidates Express HTTP API, WebSocket, MCP endpoint, terminal PTY management, and file watching into a single per-project process
  • Implement 6 new source files in packages/cli/src/backend/ (types, terminal, server, lifecycle, index) plus entry point backend-server.ts
  • Add 3 new CLI commands: backend-start, backend-stop, backend-status with dynamic imports for zero overhead on other commands

Test plan

  • All 196 existing unit tests pass
  • Build succeeds with no errors
  • Backend server starts and /api/health endpoint responds correctly
  • Biome lint passes with no issues
  • Pre-push hooks (build + lint + test) pass

🤖 Generated with Claude Code

Implement a persistent per-project backend service that consolidates
Express HTTP API, WebSocket, MCP endpoint, terminal PTY management,
and file watching into a single process.

New files:
- packages/cli/src/backend/types.ts — Type definitions
- packages/cli/src/backend/terminal.ts — PTY manager + session store
- packages/cli/src/backend/server.ts — Express app with all routes
- packages/cli/src/backend/lifecycle.ts — Start/stop/health/discovery
- packages/cli/src/backend/index.ts — Public exports
- packages/cli/src/backend-server.ts — Entry point

Modified:
- packages/cli/src/cli.ts — backend-start/stop/status commands
- packages/cli/tsdown.config.ts — backend-server build entry
- packages/cli/package.json — express, ws, chokidar, detect-port deps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 2, 2026 18:00
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a unified, persistent per-project backend process for MAXSIM, consolidating multiple services (HTTP API, WebSockets, MCP HTTP endpoint, terminal PTY, file watching) behind new CLI lifecycle commands.

Changes:

  • Add a new backend server entrypoint/bundle and supporting backend modules (server, lifecycle, terminal, types).
  • Add CLI commands to start/stop/query backend status with dynamic imports (backend-start, backend-stop, backend-status).
  • Update dependencies and regenerate dist/ artifacts + docs/assets.

Reviewed changes

Copilot reviewed 10 out of 72 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/cli/tsdown.config.ts Adds backend-server build entry and bundling/externalization settings.
packages/cli/src/cli.ts Adds backend-start/stop/status CLI commands.
packages/cli/src/backend/types.ts Defines backend config/status/message types.
packages/cli/src/backend/terminal.ts Implements PTY management + scrollback store.
packages/cli/src/backend/lifecycle.ts Implements backend start/stop/status + lockfile behavior.
packages/cli/src/backend/server.ts Implements unified Express + WS + MCP + watcher + terminal backend server.
packages/cli/src/backend/index.ts Re-exports backend types and lifecycle/server functions.
packages/cli/src/backend-server.ts Backend server process entrypoint (env-driven).
packages/cli/package.json Adds express/ws/chokidar/detect-port deps and type packages.
package-lock.json Locks updated for new deps/version bump.
packages/cli/README.md Updates docs for Claude-only + skills/MCP sections.
packages/cli/dist/skills-DYictYGI.cjs Regenerated build artifact.
packages/cli/dist/mcp-server.cjs Regenerated build artifact.
packages/cli/dist/lifecycle-B6gdn2NV.cjs.map Regenerated build artifact.
packages/cli/dist/lifecycle-B6gdn2NV.cjs Regenerated build artifact.
packages/cli/dist/install.cjs Regenerated build artifact.
packages/cli/dist/core/skills.js.map Regenerated build artifact.
packages/cli/dist/core/skills.js Regenerated build artifact.
packages/cli/dist/core/skills.d.ts.map Regenerated build artifact.
packages/cli/dist/core/skills.d.ts Regenerated build artifact.
packages/cli/dist/core-Cqn3M3eD.cjs Regenerated build artifact.
packages/cli/dist/cli.js Regenerated build artifact.
packages/cli/dist/cli.cjs Regenerated build artifact.
packages/cli/dist/backend/types.js.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/types.js Regenerated build artifact for new backend module.
packages/cli/dist/backend/types.d.ts.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/types.d.ts Regenerated build artifact for new backend module.
packages/cli/dist/backend/terminal.js.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/terminal.js Regenerated build artifact for new backend module.
packages/cli/dist/backend/terminal.d.ts.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/terminal.d.ts Regenerated build artifact for new backend module.
packages/cli/dist/backend/server.js Regenerated build artifact for new backend module.
packages/cli/dist/backend/server.d.ts.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/server.d.ts Regenerated build artifact for new backend module.
packages/cli/dist/backend/lifecycle.js.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/lifecycle.js Regenerated build artifact for new backend module.
packages/cli/dist/backend/lifecycle.d.ts.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/lifecycle.d.ts Regenerated build artifact for new backend module.
packages/cli/dist/backend/index.js.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/index.js Regenerated build artifact for new backend module.
packages/cli/dist/backend/index.d.ts.map Regenerated build artifact for new backend module.
packages/cli/dist/backend/index.d.ts Regenerated build artifact for new backend module.
packages/cli/dist/backend-server.js.map Regenerated build artifact for new entrypoint.
packages/cli/dist/backend-server.js Regenerated build artifact for new entrypoint.
packages/cli/dist/backend-server.d.ts.map Regenerated build artifact for new entrypoint.
packages/cli/dist/backend-server.d.ts Regenerated build artifact for new entrypoint.
packages/cli/dist/backend-server.d.cts Regenerated build artifact for new entrypoint.
packages/cli/dist/assets/templates/skills/verification-before-completion/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/using-maxsim/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/systematic-debugging/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/simplify/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/roadmap-writing/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/memory-management/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/code-review/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/templates/skills/brainstorming/SKILL.md Regenerated/updated skill template content.
packages/cli/dist/assets/dashboard/server.js Regenerated dashboard asset bundle.
packages/cli/dist/assets/CHANGELOG.md Regenerated packaged changelog in assets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +76 to +80
// 1. Check if already running
const existing = await getBackendStatus(projectCwd);
if (existing && existing.status === 'ok') {
return readLockFile(projectCwd)!;
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startBackend() returns readLockFile(projectCwd)! when health says the backend is running. This non-null assertion can still crash on a race (lockfile deleted between the health check and re-read). Prefer returning the lock data you already have (read once) or handle the null case by re-creating the lockfile from existing/health response.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +110
export async function startBackend(
projectCwd: string,
opts?: { port?: number; background?: boolean },
): Promise<BackendLockFile> {
// 1. Check if already running
const existing = await getBackendStatus(projectCwd);
if (existing && existing.status === 'ok') {
return readLockFile(projectCwd)!;
}

// 2. Choose port
const port = opts?.port ?? projectPort(projectCwd);
const background = opts?.background ?? true;

if (background) {
// Spawn detached child process running backend-server.cjs
const serverScript = path.join(__dirname, 'backend-server.cjs');
const child = spawn(process.execPath, [serverScript], {
cwd: projectCwd,
detached: true,
stdio: 'ignore',
env: {
...process.env,
MAXSIM_PORT: String(port),
MAXSIM_PROJECT_CWD: projectCwd,
},
...(process.platform === 'win32' ? { shell: true } : {}),
});
child.unref();

const lockData: BackendLockFile = {
pid: child.pid ?? 0,
port,
startedAt: Date.now(),
cwd: projectCwd,
};

writeLockFile(projectCwd, lockData);
return lockData;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new backend lifecycle logic (lockfile handling + start/stop/status) introduces several edge cases (stale lockfile cleanup, port selection, PID handling). There don’t appear to be unit tests covering these behaviors; adding focused tests for lockfile creation/removal and status detection would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +90
if (background) {
// Spawn detached child process running backend-server.cjs
const serverScript = path.join(__dirname, 'backend-server.cjs');
const child = spawn(process.execPath, [serverScript], {
cwd: projectCwd,
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In background mode the backend script path is resolved as path.join(__dirname, 'backend-server.cjs'). When this file is compiled to dist/backend/lifecycle.js, __dirname will be dist/backend, so this points to a non-existent dist/backend/backend-server.cjs and the spawn will fail. Resolve the script relative to the dist root instead (e.g., path.join(__dirname, '..', 'backend-server.cjs') in CJS builds, or use import.meta.url/fileURLToPath for ESM-safe resolution).

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +107
const lockData: BackendLockFile = {
pid: child.pid ?? 0,
port,
startedAt: Date.now(),
cwd: projectCwd,
};
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lockData.pid is set to child.pid ?? 0. If spawn fails before PID assignment (or on platforms where PID can be undefined), writing 0 is dangerous: stopBackend() later calls process.kill(lock.pid, 'SIGTERM'), and process.kill(0, ...) signals the entire process group on POSIX. Treat missing PID as a start failure (don’t write a lockfile), or store pid: null and guard against killing when PID is not a positive integer.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +110
// 2. Choose port
const port = opts?.port ?? projectPort(projectCwd);
const background = opts?.background ?? true;

if (background) {
// Spawn detached child process running backend-server.cjs
const serverScript = path.join(__dirname, 'backend-server.cjs');
const child = spawn(process.execPath, [serverScript], {
cwd: projectCwd,
detached: true,
stdio: 'ignore',
env: {
...process.env,
MAXSIM_PORT: String(port),
MAXSIM_PROJECT_CWD: projectCwd,
},
...(process.platform === 'win32' ? { shell: true } : {}),
});
child.unref();

const lockData: BackendLockFile = {
pid: child.pid ?? 0,
port,
startedAt: Date.now(),
cwd: projectCwd,
};

writeLockFile(projectCwd, lockData);
return lockData;
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock file records port before the child process starts, but the backend server uses detect-port and may bind to a different port if the requested one is taken. If that happens, the lock file becomes stale immediately (health checks and shutdown will hit the wrong port, and getBackendStatus() will delete the lockfile while the server is still running). Fix by selecting an available port in the parent (using detect-port here too) before spawning, or by having the child report its final bound port back so the lockfile can be written/updated with the actual port.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +65
function isWithinPlanning(cwd: string, targetPath: string): boolean {
const planningDir = path.resolve(cwd, '.planning');
const resolved = path.resolve(cwd, targetPath);
return resolved.startsWith(planningDir);
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isWithinPlanning() uses resolved.startsWith(planningDir), which is vulnerable to prefix matches like <cwd>/.planning2/... (outside the .planning/ directory) being treated as allowed. Use a boundary-aware check like resolved === planningDir || resolved.startsWith(planningDir + path.sep) (or path.relative(planningDir, resolved) and ensure it doesn’t start with ..).

Copilot uses AI. Check for mistakes.
Comment on lines +403 to +410
'backend-start': async (args, cwd, raw) => {
const { startBackend } = await import('./backend/lifecycle.js');
const portFlag = args.find(a => a.startsWith('--port='))?.split('=')[1];
const background = !args.includes('--foreground');
const result = await startBackend(cwd, {
port: portFlag ? parseInt(portFlag, 10) : undefined,
background,
});
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--port= is parsed with parseInt(portFlag, 10) and passed through even if it’s invalid (NaN) or out of range. Since NaN is not nullish, startBackend() will treat it as an explicit port and propagate it into MAXSIM_PORT, which can break detect-port / server startup. Validate the parsed number (finite integer, 1-65535) and return a CLI error when invalid.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +17
const port = parseInt(process.env.MAXSIM_PORT || '3142', 10);
const cwd = process.env.MAXSIM_PROJECT_CWD || process.cwd();

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAXSIM_PORT is parsed with parseInt(...) but not validated. If the env var is missing/invalid (or passed through as "NaN"), the backend will start with port: NaN, which can cause runtime failures when selecting/listening on a port. Add a guard (e.g., fall back to a default when Number.isNaN(port)), and consider rejecting non-numeric input with a clear stderr error + nonzero exit.

Copilot uses AI. Check for mistakes.
@maystudios maystudios merged commit abe4d38 into main Mar 2, 2026
4 checks passed
github-actions bot pushed a commit that referenced this pull request Mar 2, 2026
# [4.1.0](v4.0.2...v4.1.0) (2026-03-02)

### Features

* **backend:** add unified backend server package ([#49](#49)) ([abe4d38](abe4d38))
* **mcp:** add context, roadmap, and config query tools ([#48](#48)) ([8828c68](8828c68))
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

🎉 This PR is included in version 4.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants