Repo-side rename only (Phases 1-3 of the migration plan); the live box (~/.castle, systemd units, /data/castle, domains) is a separate cutover. - Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api), dist names, entry point `wildpc = wildpc_cli.main:main`. - Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*. - Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml -> wildpc.yaml, /data/castle -> /data/wildpc. - Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc. - Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend. - Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated; redundant nested uv.lock files dropped (workspace root lock is authoritative). Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds. Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
// Resolve the wildpc-api base URL. The gateway serves each service at its own
|
|
// subdomain (<name>.<domain>), so when the dashboard runs at wildpc.<domain>
|
|
// the API lives at wildpc-api.<domain> — a cross-origin call (wildpc-api allows
|
|
// CORS *). When served at a bare host (dev, or the off-mode :9000 gateway), the
|
|
// API is reachable same-origin at /api.
|
|
function resolveApiBase(): string {
|
|
const configured = import.meta.env.VITE_API_BASE_URL
|
|
if (configured) return configured
|
|
if (typeof window !== "undefined") {
|
|
const { protocol, hostname } = window.location
|
|
const labels = hostname.split(".")
|
|
if (labels.length > 2) {
|
|
return `${protocol}//wildpc-api.${labels.slice(1).join(".")}`
|
|
}
|
|
}
|
|
return "/api"
|
|
}
|
|
|
|
const BASE_URL = resolveApiBase()
|
|
|
|
class ApiError extends Error {
|
|
status: number
|
|
constructor(status: number, message: string) {
|
|
super(message)
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
class ApiClient {
|
|
private baseUrl: string
|
|
|
|
constructor(baseUrl = BASE_URL) {
|
|
this.baseUrl = baseUrl
|
|
}
|
|
|
|
async get<T>(path: string): Promise<T> {
|
|
const resp = await fetch(`${this.baseUrl}${path}`)
|
|
if (!resp.ok) {
|
|
throw new ApiError(resp.status, await resp.text())
|
|
}
|
|
return resp.json()
|
|
}
|
|
|
|
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
method: "POST",
|
|
headers: body ? { "Content-Type": "application/json" } : {},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
})
|
|
if (!resp.ok) {
|
|
throw new ApiError(resp.status, await resp.text())
|
|
}
|
|
return resp.json()
|
|
}
|
|
|
|
async put<T>(path: string, body?: unknown): Promise<T> {
|
|
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
method: "PUT",
|
|
headers: body ? { "Content-Type": "application/json" } : {},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
})
|
|
if (!resp.ok) {
|
|
throw new ApiError(resp.status, await resp.text())
|
|
}
|
|
return resp.json()
|
|
}
|
|
|
|
async delete<T>(path: string): Promise<T> {
|
|
const resp = await fetch(`${this.baseUrl}${path}`, { method: "DELETE" })
|
|
if (!resp.ok) {
|
|
throw new ApiError(resp.status, await resp.text())
|
|
}
|
|
return resp.json()
|
|
}
|
|
|
|
streamUrl(path: string): string {
|
|
return `${this.baseUrl}${path}`
|
|
}
|
|
|
|
// Build a ws(s):// URL for a WebSocket endpoint. Handles both the cross-origin
|
|
// base (https://wildpc-api.<domain>) and the same-origin "/api" base.
|
|
wsUrl(path: string): string {
|
|
const absolute = this.baseUrl.startsWith("http")
|
|
? `${this.baseUrl}${path}`
|
|
: `${window.location.origin}${this.baseUrl}${path}`
|
|
const url = new URL(absolute)
|
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
|
return url.toString()
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient()
|
|
export { ApiError }
|