Skip to content

Add daemonized web gateway behavior for tithe web#9

Merged
sergdort merged 2 commits intomainfrom
codex/web-daemon-gateway
Mar 2, 2026
Merged

Add daemonized web gateway behavior for tithe web#9
sergdort merged 2 commits intomainfrom
codex/web-daemon-gateway

Conversation

@sergdort
Copy link
Owner

@sergdort sergdort commented Mar 2, 2026

Summary

  • default tithe web to mode preview for build-first serving behavior
  • add daemon lifecycle controls: --daemon, --status, and --stop
  • add detached supervisor with restart-on-crash backoff for API/PWA child processes
  • include local plus Tailnet access metadata in startup/status payloads with warning fallback when Tailscale is unavailable
  • document the new web runtime behavior in README.md and AGENTS.md

Validation

  • pnpm --filter @tithe/cli typecheck
  • pnpm --filter @tithe/cli build
  • pnpm exec biome check apps/cli/src/web.ts apps/cli/src/index.ts tests/cli/web.spec.ts
  • manual tithe web --daemon / --status / --stop verification

Notes

  • tests/cli/web.spec.ts was updated for new CLI option validation paths
  • pnpm exec vitest --config vitest.config.ts cli/web.spec.ts is currently blocked in this environment by a pre-existing better-sqlite3 native ABI mismatch

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

This PR updates the tithe web CLI workflow to support running the API + PWA stack under a detached “daemon supervisor” process, with lifecycle controls for status/stop and enriched startup metadata (local + best-effort Tailnet URLs).

Changes:

  • Add --daemon, --status, and --stop control flags to tithe web, plus an internal web-supervisor command used for background supervision.
  • Implement daemon state/pid/log file management and supervisor auto-restart backoff logic for API/PWA child processes.
  • Update docs and add CLI validation tests for incompatible flag combinations.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/cli/src/web.ts Implements daemon/supervisor lifecycle, state persistence, access metadata (Tailscale), and option validation.
apps/cli/src/index.ts Exposes new web flags and adds the internal web-supervisor command wiring.
tests/cli/web.spec.ts Adds coverage for new validation paths around daemon control flags.
README.md Documents new default mode and daemon lifecycle behavior.
AGENTS.md Updates “Web runtime” command docs and CLI behavior notes for daemon support.
.agents/notes.md Adds internal notes about the shim behavior and daemon metadata path resolution.

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

Comment on lines +636 to +637
fs.writeFileSync(tempFile, JSON.stringify(value, null, 2));
fs.renameSync(tempFile, filePath);
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.

writeJsonFile uses fs.renameSync(tempFile, filePath) to update the daemon state atomically, but rename cannot overwrite an existing destination file on Windows. After the first write, subsequent persistDaemonState calls can throw and crash the supervisor. Consider using an overwrite-capable approach on win32 (e.g., delete/replace destination first, or fallback to writeFileSync(filePath, ...) when renameSync fails with EEXIST/EPERM).

Suggested change
fs.writeFileSync(tempFile, JSON.stringify(value, null, 2));
fs.renameSync(tempFile, filePath);
const data = JSON.stringify(value, null, 2);
fs.writeFileSync(tempFile, data);
try {
if (process.platform === 'win32' && fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
fs.renameSync(tempFile, filePath);
} catch (error: any) {
try {
const code = (error as NodeJS.ErrnoException).code;
if (
process.platform === 'win32' &&
(code === 'EEXIST' || code === 'EPERM')
) {
// Fallback: overwrite the destination directly on Windows
fs.writeFileSync(filePath, data);
fs.unlinkSync(tempFile);
return;
}
} finally {
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
}
}
throw error;
}

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 9197614. writeJsonFile now handles Windows rename edge cases by unlinking destination first on win32 and falling back to direct overwrite for EEXIST/EPERM, with temp-file cleanup in finally.

Comment on lines +538 to +540
const status = spawnSync('tailscale', ['status', '--json'], {
encoding: 'utf8',
});
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.

spawnSync('tailscale', ...) has no timeout, so a hung tailscale status --json call can block tithe web startup/status output indefinitely (including in foreground mode). Consider setting a reasonable timeout (and checking status.signal) to ensure the CLI remains responsive when Tailscale is slow/unresponsive.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 9197614. tailscale status calls now use a 2000ms timeout and we surface a timeout warning when the command is terminated by signal, so startup and status do not block indefinitely.

Comment on lines +979 to +1003
const cleanup = () => {
process.off('SIGINT', handleSignal);
process.off('SIGTERM', handleSignal);
for (const timer of restartTimers.values()) {
clearTimeout(timer);
}
restartTimers.clear();
};

const finishResolve = () => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve();
};

const finishReject = (error: AppError) => {
if (settled) {
return;
}
settled = true;
cleanup();
reject(error);
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.

cleanup()/finishReject() in the supervisor loop clear timers and handlers but do not stop any already-running child services. If the supervisor exits due to a fatal error path, API/PWA processes can be orphaned while the pid file is removed, making --stop ineffective and leaving ports occupied. Consider stopping/killing children on reject (and/or adding process.on('exit'|'uncaughtException'|'unhandledRejection') to ensure children are terminated).

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 9197614. finishReject now marks shutdown and sends SIGTERM to running API/PWA children before rejecting, so fatal supervisor paths no longer leave orphan processes behind.

@sergdort sergdort merged commit be02338 into main Mar 2, 2026
1 check passed
@sergdort sergdort deleted the codex/web-daemon-gateway branch March 2, 2026 21:56
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.

2 participants