Files
owenandCursor bb9999de58 Package Pi Mobile for developer distribution.
Publish the hub as pi-mobile-hub with CLI flags and LAN pairing auth, add mobile deep-link pairing and EAS build config, and expand gitignore for Expo/EAS artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 14:54:09 -05:00

253 lines
6.8 KiB
TypeScript

import AsyncStorage from "@react-native-async-storage/async-storage";
import type { HostConfig, ModelRef, SavedComputer } from "./types";
const HOST_KEY = "pi-mobile:last-host";
const COMPUTERS_KEY = "pi-mobile:computers";
const PROJECTS_PREFIX = "pi-mobile:projects:";
const LAST_MODEL_KEY = "pi-mobile:last-model";
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
function computerKey(host: string, port: string): string {
return `${host.trim()}:${port.trim() || "8787"}`;
}
function sortByRecent(computers: SavedComputer[]): SavedComputer[] {
return [...computers].sort((a, b) => b.lastUsedAt - a.lastUsedAt);
}
function projectsStorageKey(host: string, port: string): string {
return `${PROJECTS_PREFIX}${host.trim()}:${port.trim() || "8787"}`;
}
export function buildBaseUrl(host: string, port: string): string {
const trimmedHost = host.trim();
const trimmedPort = port.trim() || "8787";
const withProtocol = trimmedHost.startsWith("http") ? trimmedHost : `http://${trimmedHost}`;
const url = new URL(withProtocol);
url.port = trimmedPort;
return url.origin;
}
export async function loadHostConfig(): Promise<HostConfig | null> {
const raw = await AsyncStorage.getItem(HOST_KEY);
if (!raw) {
return null;
}
try {
return JSON.parse(raw) as HostConfig;
} catch {
return null;
}
}
export async function saveHostConfig(config: HostConfig): Promise<void> {
await AsyncStorage.setItem(HOST_KEY, JSON.stringify(config));
}
export async function loadLastModel(): Promise<ModelRef | null> {
const raw = await AsyncStorage.getItem(LAST_MODEL_KEY);
if (!raw) {
return null;
}
try {
const parsed = JSON.parse(raw) as Partial<ModelRef>;
return typeof parsed.provider === "string" && typeof parsed.id === "string"
? { provider: parsed.provider, id: parsed.id }
: null;
} catch {
return null;
}
}
export async function saveLastModel(model: ModelRef): Promise<void> {
await AsyncStorage.setItem(LAST_MODEL_KEY, JSON.stringify(model));
}
export async function loadSavedComputers(): Promise<SavedComputer[]> {
const raw = await AsyncStorage.getItem(COMPUTERS_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as unknown;
if (Array.isArray(parsed)) {
return sortByRecent(
parsed.filter(
(entry): entry is SavedComputer =>
typeof entry === "object" &&
entry !== null &&
typeof (entry as SavedComputer).id === "string" &&
typeof (entry as SavedComputer).name === "string" &&
typeof (entry as SavedComputer).host === "string" &&
typeof (entry as SavedComputer).port === "string" &&
typeof (entry as SavedComputer).lastUsedAt === "number",
),
);
}
} catch {
// fall through to migration
}
}
const legacy = await loadHostConfig();
if (!legacy?.host.trim()) {
return [];
}
const migrated: SavedComputer = {
id: generateId(),
name: legacy.host.trim(),
host: legacy.host.trim(),
port: legacy.port.trim() || "8787",
lastUsedAt: Date.now(),
};
await saveSavedComputers([migrated]);
return [migrated];
}
export async function saveSavedComputers(computers: SavedComputer[]): Promise<void> {
await AsyncStorage.setItem(COMPUTERS_KEY, JSON.stringify(sortByRecent(computers)));
}
export async function upsertSavedComputer(
computers: SavedComputer[],
host: string,
port: string,
name?: string,
token?: string,
): Promise<SavedComputer[]> {
const trimmedHost = host.trim();
const trimmedPort = port.trim() || "8787";
if (!trimmedHost) {
return computers;
}
const key = computerKey(trimmedHost, trimmedPort);
const existing = computers.find((entry) => computerKey(entry.host, entry.port) === key);
const trimmedName = name?.trim();
const trimmedToken = token?.trim();
if (existing) {
const next = computers.map((entry) =>
entry.id === existing.id
? {
...entry,
name: trimmedName || entry.name,
host: trimmedHost,
port: trimmedPort,
token: trimmedToken || entry.token,
lastUsedAt: Date.now(),
}
: entry,
);
await saveSavedComputers(next);
return sortByRecent(next);
}
const next = [
{
id: generateId(),
name: trimmedName || trimmedHost,
host: trimmedHost,
port: trimmedPort,
token: trimmedToken,
lastUsedAt: Date.now(),
},
...computers,
];
await saveSavedComputers(next);
return sortByRecent(next);
}
export async function removeSavedComputer(
computers: SavedComputer[],
id: string,
): Promise<SavedComputer[]> {
const next = computers.filter((entry) => entry.id !== id);
await saveSavedComputers(next);
return next;
}
export async function loadSavedProjects(host: string, port: string): Promise<string[]> {
const raw = await AsyncStorage.getItem(projectsStorageKey(host, port));
if (!raw) {
return [];
}
try {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) {
return [];
}
return parsed.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
} catch {
return [];
}
}
export async function saveSavedProjects(
host: string,
port: string,
projects: string[],
): Promise<void> {
await AsyncStorage.setItem(projectsStorageKey(host, port), JSON.stringify(projects));
}
function authHeaders(token?: string): HeadersInit | undefined {
if (!token) {
return undefined;
}
return {
"x-pi-mobile-token": token,
};
}
export async function checkHealth(baseUrl: string, token?: string): Promise<boolean> {
const response = await fetch(`${baseUrl}/health`, {
headers: authHeaders(token),
});
if (!response.ok) {
return false;
}
const data = (await response.json()) as { ok?: boolean };
return data.ok === true;
}
export async function listFolders(baseUrl: string, path = "~", token?: string) {
const response = await fetch(`${baseUrl}/fs/list?path=${encodeURIComponent(path)}`, {
headers: authHeaders(token),
});
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error ?? "Failed to list folders");
}
return response.json();
}
export async function createFolder(
baseUrl: string,
path: string,
name: string,
token?: string,
): Promise<{ name: string; path: string }> {
const response = await fetch(`${baseUrl}/fs/mkdir`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...authHeaders(token),
},
body: JSON.stringify({ path, name }),
});
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error ?? "Failed to create folder");
}
return response.json();
}