/** * Centralized URL path builder. All navigation in shared packages (packages/views) * MUST go through this module — no hardcoded string paths. * * Two kinds of paths: * - workspace-scoped: paths.workspace(slug).xxx() — carry workspace in URL * - global: paths.login(), paths.newWorkspace(), paths.invite(id) — pre-workspace routes * * Why pure functions + builder pattern: * - Changing a route shape (e.g. adding workspace slug prefix) becomes a single-file edit * - IDs are always URL-encoded here so callers can't forget * - Zero runtime deps means this module is safe in Node (tests) and browsers */ const encode = (id: string) => encodeURIComponent(id); function workspaceScoped(slug: string) { const ws = `/${encode(slug)}`; return { root: () => `${ws}/issues`, issues: () => `${ws}/issues`, issueDetail: (id: string) => `${ws}/issues/${encode(id)}`, projects: () => `${ws}/projects`, projectDetail: (id: string) => `${ws}/projects/${encode(id)}`, autopilots: () => `${ws}/autopilots`, autopilotDetail: (id: string) => `${ws}/autopilots/${encode(id)}`, agents: () => `${ws}/agents`, inbox: () => `${ws}/inbox`, myIssues: () => `${ws}/my-issues`, runtimes: () => `${ws}/runtimes`, skills: () => `${ws}/skills`, settings: () => `${ws}/settings`, }; } export const paths = { workspace: workspaceScoped, // Global (pre-workspace) routes login: () => "/login", newWorkspace: () => "/workspaces/new", invite: (id: string) => `/invite/${encode(id)}`, authCallback: () => "/auth/callback", root: () => "/", }; export type WorkspacePaths = ReturnType; // Prefixes — not slug names — because we match against full URL paths. // A path is global if it equals or begins with any of these. // Note: `/workspaces/` (trailing slash) is the prefix — `workspaces` is reserved, // so any path starting with `/workspaces/...` is system-owned, not user-owned. const GLOBAL_PREFIXES = ["/login", "/workspaces/", "/invite/", "/auth/", "/logout", "/signup"]; export function isGlobalPath(path: string): boolean { return GLOBAL_PREFIXES.some((p) => path === p || path.startsWith(p)); }