Skip to content

feat: create ppg serve command and Fastify server scaffold#94

Open
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-63-serve-cmd
Open

feat: create ppg serve command and Fastify server scaffold#94
2witstudios wants to merge 3 commits intomainfrom
ppg/issue-63-serve-cmd

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 27, 2026

Summary

  • Adds ppg serve command with --port, --host, --token, --daemon, --json options
  • Creates Fastify server scaffold with CORS, plugin registration hooks, and bearer token auth
  • Health endpoint: GET /health returns { status: "ok", uptime, version }
  • Graceful shutdown on SIGTERM/SIGINT with automatic state file cleanup
  • State file (serve.json) and PID file written with 0o600 permissions
  • LAN IP detection via os.networkInterfaces()
  • Path helpers: serveStatePath(), servePidPath() in lib/paths.ts
  • Lazy import registration in cli.ts

Closes #63

Test plan

  • npm run typecheck passes (pre-existing TS error in spawn.test.ts is unrelated)
  • npm test — all 218 tests pass
  • ppg serve starts server on default port 3100
  • ppg serve --port 8080 --host 0.0.0.0 binds correctly
  • GET /health returns ok response
  • --token flag enables bearer auth on non-health routes
  • serve.json and serve.pid written with 0o600 permissions
  • SIGINT/SIGTERM cleanly shuts down and removes state files

Summary by CodeRabbit

  • New Features
    • Added a serve command to start a development server with configurable port and host settings
    • Optional Bearer token authentication for API request security
    • Health check endpoint for monitoring server status and uptime
    • Automatic LAN address detection for network access
    • JSON output format support for automation and integration
    • Graceful shutdown handling with proper cleanup

- Add `ppg serve` command with --port, --host, --token, --daemon, --json options
- Create Fastify server with CORS, /health endpoint, bearer token auth
- Graceful shutdown on SIGTERM/SIGINT with state file cleanup
- State file (serve.json) and PID file with 0o600 permissions
- LAN IP detection via os.networkInterfaces()
- Path helpers: serveStatePath(), servePidPath() in lib/paths.ts
- Lazy import registration in cli.ts

Closes #63
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 59 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 1940e2d and ae11701.

📒 Files selected for processing (6)
  • src/cli.ts
  • src/commands/serve.ts
  • src/commands/spawn.test.ts
  • src/lib/paths.test.ts
  • src/server/index.test.ts
  • src/server/index.ts
📝 Walkthrough

Walkthrough

The changes introduce a new ppg serve command that initializes and runs an HTTP server using Fastify, with features including project root validation, CORS support, optional Bearer token authentication, a health endpoint, graceful shutdown handling, and state file persistence.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added fastify (^5.7.4) and @fastify/cors (^11.2.0) dependencies for HTTP server and cross-origin support.
CLI Integration
src/cli.ts
Registered new serve command with options for --port, --host, --token, --daemon, and --json flags; delegates to serveCommand handler.
Command Logic
src/commands/serve.ts
Implemented serveCommand function that resolves project root via Git, validates initialization, applies port/host defaults, and invokes startServer.
Path Utilities
src/lib/paths.ts
Added serveStatePath() and servePidPath() helpers to resolve serve.json and serve.pid file locations within the per-project ppg directory.
Server Core
src/server/index.ts
Implemented Fastify server with CORS registration, optional Bearer token authentication (excluding /health), health endpoint returning status/uptime/version, LAN address detection, state file persistence (pid, port, host, metadata), and graceful SIGTERM/SIGINT shutdown.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI (src/cli.ts)
    participant Cmd as Command (src/commands/serve.ts)
    participant Server as Server (src/server/index.ts)
    participant FS as File System
    
    User->>CLI: ppg serve --port 3100 --token xyz
    CLI->>Cmd: serveCommand(options)
    Cmd->>Cmd: resolveProjectRoot() via git
    Cmd->>FS: Verify project initialized
    Cmd->>Server: startServer({projectRoot, port, host, token, json})
    Server->>Server: detectLanAddress()
    Server->>Server: Initialize Fastify instance
    Server->>Server: Register CORS plugin
    Server->>Server: Register token auth middleware
    Server->>Server: Register /health GET route
    Server->>FS: Write serve.json (state)
    Server->>FS: Write serve.pid
    Server->>CLI: Log startup info or JSON
    Server->>Server: Listen on port + host
    User->>Server: SIGTERM/SIGINT signal
    Server->>FS: Remove state files
    Server->>Server: Close server gracefully
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A server springs forth, swift and fair,
With Fastify's breeze and CORS care,
Health checks hop through, tokens stand guard,
State files tucked safe, the work isn't hard!
From root to the LAN, the connection takes flight—
Our serve command's here, and it feels just right!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR implements most acceptance criteria from issue #63 but appears to be missing QR code display on startup, which is an explicit requirement in the issue's acceptance criteria. Verify whether QR code display was intentionally deferred or if implementation is incomplete. Add QR code generation and display on server startup per issue #63 requirements.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new 'ppg serve' command with a Fastify server scaffold, which aligns with the core objectives of this pull request.
Description check ✅ Passed The description comprehensively covers the 'What', 'Why', and 'How' sections with detailed implementation details, includes a test plan, and follows the required template structure.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the 'ppg serve' command and Fastify server scaffold per issue #63 requirements; no unrelated or out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/issue-63-serve-cmd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli.ts`:
- Line 288: Replace the loose coercion in the CLI .option for port with a strict
parser: implement a parsePort(v: string) function that converts v to a number,
validates Number.isInteger(n) and n is between 1 and 65535, and throws an Error
with a clear message if validation fails; then pass parsePort as the third
argument to the existing .option('-p, --port <number>', ..., parsePort, 3100) so
invalid ports are rejected at parse time (refer to the parsePort function and
the .option call).

In `@src/commands/serve.ts`:
- Around line 33-46: serveCommand currently ignores ServeCommandOptions.daemon;
update it to honor the flag by launching the server in background when
options.daemon is true. Implement by either (A) passing daemon through to
startServer (add a daemon parameter to startServer and propagate it from
serveCommand) or (B) spawning a detached child process that re-runs the CLI with
the same args (use child_process.spawn with detached: true, stdio: 'ignore',
unref()) and exit the parent process. Ensure you reference and modify
ServeCommandOptions, serveCommand, and startServer (or the new helper that
spawns the detached process) so the CLI's --daemon behavior is actually
implemented.

In `@src/server/index.ts`:
- Around line 113-123: Startup output currently omits the QR payload; when not
running with --json and a token exists you should generate and display a
terminal QR for the connection URL+token and include the QR payload in the
printed state when --json is used. In practice, update the block that checks
json/host/port/token (variables: json, host, port, token, lanAddress, state) so
that: if json is true, add a qr_payload field to state (the URL including token
and LAN address variant if present) and JSON.stringify(state) as before; if json
is false and token is present, call the terminal QR generator (e.g., a utility
function you add like renderTerminalQr(qr_payload)) to print the QR after the
success/info logs and also info() the plain URL; ensure behavior remains
unchanged when token is falsy (no QR generation) and that LAN address URL is
used when lanAddress is present.
- Around line 42-50: The writeStateFile and writePidFile functions only pass
mode: 0o600 to fs.writeFile which affects permissions on creation but does not
change existing file permissions; after writing the file (statePath and pidPath)
call fs.chmod(statePath, 0o600) and fs.chmod(pidPath, 0o600) respectively (await
the calls and handle errors as appropriate) to ensure existing files are
hardened to 0o600 regardless of prior permissions.
- Around line 3-10: Replace the CommonJS interop usage by removing the
createRequire import and the const require = createRequire(import.meta.url)
declaration, and instead import the package JSON directly as pkg using an ESM
JSON import (so that pkg.version is available); update any references to pkg
(currently declared via require('../../package.json')) accordingly and delete
the unused createRequire symbol and the require variable to keep the module
ESM-native.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34deb69 and 1940e2d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • src/cli.ts
  • src/commands/serve.ts
  • src/lib/paths.ts
  • src/server/index.ts

Comment on lines 33 to 46
export async function serveCommand(options: ServeCommandOptions): Promise<void> {
const projectRoot = await resolveProjectRoot();

const port = options.port ?? 3100;
const host = options.host ?? '127.0.0.1';

await startServer({
projectRoot,
port,
host,
token: options.token,
json: options.json,
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--daemon is declared but not implemented.

ServeCommandOptions includes daemon (Line 29), but serveCommand ignores it (Line 39-Line 45). The CLI advertises daemon mode, so this currently behaves as a silent no-op.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/serve.ts` around lines 33 - 46, serveCommand currently ignores
ServeCommandOptions.daemon; update it to honor the flag by launching the server
in background when options.daemon is true. Implement by either (A) passing
daemon through to startServer (add a daemon parameter to startServer and
propagate it from serveCommand) or (B) spawning a detached child process that
re-runs the CLI with the same args (use child_process.spawn with detached: true,
stdio: 'ignore', unref()) and exit the parent process. Ensure you reference and
modify ServeCommandOptions, serveCommand, and startServer (or the new helper
that spawns the detached process) so the CLI's --daemon behavior is actually
implemented.

Comment on lines 3 to 10
import { createRequire } from 'node:module';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { serveStatePath, servePidPath } from '../lib/paths.js';
import { info, success, warn } from '../lib/output.js';

const require = createRequire(import.meta.url);
const pkg = require('../../package.json') as { version: string };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Find and display tsconfig to check JSON import support
echo "=== Searching for tsconfig files ==="
fd tsconfig --type f

echo -e "\n=== Content of tsconfig files ==="
fd tsconfig --type f -x cat {}

echo -e "\n=== CommonJS patterns in src/server/index.ts ==="
rg -n "createRequire|require\(" src/server/index.ts -A 2

Repository: 2witstudios/ppg-cli

Length of output: 958


Replace createRequire/require with ESM-native JSON import.

Lines 3, 9–10 use CommonJS interop to load the package version. Replace with:

import pkg from '../../package.json' assert { type: 'json' };

Then remove the createRequire import and const require declaration. The tsconfig already enables resolveJsonModule, making this ESM-compliant approach available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` around lines 3 - 10, Replace the CommonJS interop usage
by removing the createRequire import and the const require =
createRequire(import.meta.url) declaration, and instead import the package JSON
directly as pkg using an ESM JSON import (so that pkg.version is available);
update any references to pkg (currently declared via
require('../../package.json')) accordingly and delete the unused createRequire
symbol and the require variable to keep the module ESM-native.

Comment on lines +42 to +50
async function writeStateFile(projectRoot: string, state: ServeState): Promise<void> {
const statePath = serveStatePath(projectRoot);
await fs.writeFile(statePath, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
}

async function writePidFile(projectRoot: string, pid: number): Promise<void> {
const pidPath = servePidPath(projectRoot);
await fs.writeFile(pidPath, String(pid) + '\n', { mode: 0o600 });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden file permissions for existing serve.json/serve.pid.

Line 44 and Line 49 pass mode: 0o600, but this only applies on file creation. If files already exist, prior permissions may persist.

Suggested fix
 async function writeStateFile(projectRoot: string, state: ServeState): Promise<void> {
   const statePath = serveStatePath(projectRoot);
   await fs.writeFile(statePath, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
+  await fs.chmod(statePath, 0o600);
 }
 
 async function writePidFile(projectRoot: string, pid: number): Promise<void> {
   const pidPath = servePidPath(projectRoot);
   await fs.writeFile(pidPath, String(pid) + '\n', { mode: 0o600 });
+  await fs.chmod(pidPath, 0o600);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` around lines 42 - 50, The writeStateFile and
writePidFile functions only pass mode: 0o600 to fs.writeFile which affects
permissions on creation but does not change existing file permissions; after
writing the file (statePath and pidPath) call fs.chmod(statePath, 0o600) and
fs.chmod(pidPath, 0o600) respectively (await the calls and handle errors as
appropriate) to ensure existing files are hardened to 0o600 regardless of prior
permissions.

Comment on lines +113 to +123
if (json) {
console.log(JSON.stringify(state));
} else {
success(`Server listening on http://${host}:${port}`);
if (lanAddress) {
info(`LAN address: http://${lanAddress}:${port}`);
}
if (token) {
info('Bearer token authentication enabled');
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Startup flow is missing the QR code output required by the issue objective.

The startup output currently prints text/JSON only; no QR payload is generated/displayed for token + connection URL.

If you want, I can draft a minimal QR integration patch (terminal QR + JSON-safe behavior under --json).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/index.ts` around lines 113 - 123, Startup output currently omits
the QR payload; when not running with --json and a token exists you should
generate and display a terminal QR for the connection URL+token and include the
QR payload in the printed state when --json is used. In practice, update the
block that checks json/host/port/token (variables: json, host, port, token,
lanAddress, state) so that: if json is true, add a qr_payload field to state
(the URL including token and LAN address variant if present) and
JSON.stringify(state) as before; if json is false and token is present, call the
terminal QR generator (e.g., a utility function you add like
renderTerminalQr(qr_payload)) to print the QR after the success/info logs and
also info() the plain URL; ensure behavior remains unchanged when token is falsy
(no QR generation) and that LAN address URL is used when lanAddress is present.

- Use crypto.timingSafeEqual() for bearer token comparison (timing attack)
- Return reply from auth hook after 401 to halt request processing
- Replace hand-rolled resolveProjectRoot with getRepoRoot + requireManifest
- Remove unimplemented --daemon flag from CLI and options interface
- Add port validation (integer, 1-65535) via parsePort helper
- Remove duplicate defaults (Commander already provides them)
- Handle unhandled promise rejection in signal shutdown handlers
- Remove unused warn import from server/index.ts
- Add tests: timingSafeTokenMatch (6), detectLanAddress (3), path helpers (2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create ppg serve command and Fastify server scaffold

1 participant