Files
backnotprop__plannotator/packages/server/sessions.ts
T
Hrand Liu e0aee7451b feat: add PLANNOTATOR_DATA_DIR env var to customize data directory (#795)
* feat: add PLANNOTATOR_DATA_DIR env var to customize data directory

* fix: update missed hardcoded paths to use PLANNOTATOR_DATA_DIR

OpenCode plugin and VS Code extension still used hardcoded
~/.plannotator paths, causing the IPC registry and plan backing
file to diverge from the server when PLANNOTATOR_DATA_DIR is set.

Also exports data-dir from @plannotator/shared and documents the
new env var in AGENTS.md.

Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>

* fix: vendor data-dir.ts into Pi extension and rewrite imports

The Pi extension copies shared/server modules into generated/ at
build time. Without vendoring data-dir.ts and rewriting the
parent-relative imports, typecheck fails on all generated files
that import getPlannotatorDataDir.

* refactor: eliminate duplicated data-dir logic and clean up call sites

- VS Code extension: replace inlined getPlannotatorDataDir() copy with
  import from the canonical packages/shared/data-dir.ts (esbuild bundles
  it, so no runtime dependency needed)
- storage.ts: hoist repeated getPlannotatorDataDir() calls to a
  module-level DATA_DIR constant, matching the pattern config.ts uses
- data-dir.ts: remove inaccurate docstring claim about relative path
  resolution (the code does not call resolve())
- improvement-hooks.ts: hoist to DATA_DIR constant, clarify comments
  on the two-level hook lookup (hooks/ subdir vs root fallback)

* fix: resolve relative PLANNOTATOR_DATA_DIR to absolute path

A relative value like ./data would break readArchivedPlan's path
traversal guard, which compares a resolve()'d absolute path against
the still-relative planDir prefix. Always return an absolute path
so all callers get consistent path shapes.

* fix: use @plannotator/shared/data-dir imports in server package

Switch from relative ../shared/data-dir imports to the package
export, matching the convention every other server file follows.
Update Pi vendor script sed rules to match the new import style.

* fix: use package imports in server and respect data dir in compound skill

Server modules: switch from relative ../shared/data-dir imports to
@plannotator/shared/data-dir, matching the convention every other
server file follows. Update Pi vendor script sed rules to match.

Compound skill: update hardcoded ~/.plannotator paths to check
PLANNOTATOR_DATA_DIR first, so the skill reads plans and writes
the improvement hook to the correct location when users set a
custom data directory.

Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>

* fix: remove remaining hardcoded ~/.plannotator assumptions

- Settings UI: replace hardcoded path in label and placeholder with
  generic text that doesn't assume a specific data directory
- quickLabels: update agent tip to reference PLANNOTATOR_DATA_DIR
  so the agent checks the correct plans directory
- codex-review: hoist getPlannotatorDataDir() to module-level DATA_DIR
  constant, eliminating redundant per-call resolution in debugLog()
- Tests: make submit-plan and storage tests resilient to
  PLANNOTATOR_DATA_DIR being set in the environment
- Install scripts (sh, ps1, cmd): check PLANNOTATOR_DATA_DIR before
  falling back to ~/.plannotator for config.json attestation lookup

* fix: expand tilde in install script and update test assertions

install.sh: PLANNOTATOR_DATA_DIR set to ~/... stays literal inside
double quotes, so the config file check silently failed. Add case
statement to expand ~ the same way the runtime data-dir.ts does.

install.test.ts: update three assertions that checked for hardcoded
~/.plannotator paths — now verify PLANNOTATOR_DATA_DIR awareness
instead.

* docs: add PLANNOTATOR_DATA_DIR to env var reference with VS Code note

Document the new env var on the marketing site's environment
variables reference page. Include a footnote about ensuring
VS Code inherits the variable when launched from the Dock.

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
2026-05-26 14:56:07 -07:00

112 lines
2.4 KiB
TypeScript

/**
* Session Registry
*
* Tracks active Plannotator server sessions in ~/.plannotator/sessions/
* so users can discover and reopen closed browser tabs.
*/
import { join } from "path";
import {
mkdirSync,
writeFileSync,
readFileSync,
readdirSync,
unlinkSync,
existsSync,
} from "fs";
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";
export interface SessionInfo {
pid: number;
port: number;
url: string;
mode: "plan" | "review" | "annotate" | "archive" | "goal-setup";
project: string;
startedAt: string;
label: string;
}
function getSessionsDir(): string {
const dir = join(getPlannotatorDataDir(), "sessions");
mkdirSync(dir, { recursive: true });
return dir;
}
function sessionPath(pid: number): string {
return join(getSessionsDir(), `${pid}.json`);
}
/**
* Check if a process is still alive.
*/
function isAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
/**
* Register the current server session.
*/
export function registerSession(info: SessionInfo): void {
writeFileSync(sessionPath(info.pid), JSON.stringify(info, null, 2), "utf-8");
}
/**
* Unregister the current process's session. No-op if not found.
*/
export function unregisterSession(pid: number = process.pid): void {
const filePath = sessionPath(pid);
try {
if (existsSync(filePath)) unlinkSync(filePath);
} catch {
// Ignore delete failures
}
}
/**
* List all active sessions. Automatically removes stale entries.
*/
export function listSessions(): SessionInfo[] {
const dir = getSessionsDir();
const active: SessionInfo[] = [];
let entries: string[];
try {
entries = readdirSync(dir);
} catch {
return [];
}
for (const entry of entries) {
if (!entry.endsWith(".json")) continue;
const filePath = join(dir, entry);
try {
const data: SessionInfo = JSON.parse(readFileSync(filePath, "utf-8"));
if (isAlive(data.pid)) {
active.push(data);
} else {
// Stale session — clean up
try {
unlinkSync(filePath);
} catch {}
}
} catch {
// Corrupt file — remove it
try {
unlinkSync(filePath);
} catch {}
}
}
// Sort by most recent first
return active.sort(
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
);
}