import { createInterface } from "node:readline"; import { readConfig } from "~/services/config/facade.js"; import { leave as leaveMesh } from "~/services/mesh/facade.js"; import { getStoredToken } from "~/services/auth/facade.js"; import { request } from "~/services/api/facade.js"; import { URLS } from "~/constants/urls.js"; import { render } from "~/ui/render.js"; import { bold, clay, dim, red } from "~/ui/styles.js"; import { EXIT } from "~/constants/exit-codes.js"; const BROKER_HTTP = URLS.BROKER.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", ""); function prompt(question: string): Promise { const rl = createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(question, (a) => { rl.close(); resolve(a.trim()); }); }); } function getUserId(token: string): string { try { const payload = JSON.parse(Buffer.from(token.split(".")[1]!, "base64url").toString()) as { sub?: string }; return payload.sub ?? ""; } catch { return ""; } } async function isOwner(slug: string, auth: { session_token: string }): Promise { try { const res = await request<{ meshes: Array<{ slug: string; is_owner: boolean }> }>({ path: `/cli/meshes`, baseUrl: BROKER_HTTP, token: auth.session_token, }); return res.meshes?.find((m) => m.slug === slug)?.is_owner ?? false; } catch { return false; } } export async function deleteMesh(slug: string, opts: { yes?: boolean } = {}): Promise { const config = readConfig(); if (!slug) { if (config.meshes.length === 0) { render.err("No meshes to remove."); return EXIT.NOT_FOUND; } render.section("select mesh to remove"); config.meshes.forEach((m, i) => { process.stdout.write(` ${bold(String(i + 1) + ")")} ${clay(m.slug)}\n`); }); render.blank(); const choice = await prompt(` ${dim("choice:")} `); const idx = parseInt(choice, 10) - 1; if (idx < 0 || idx >= config.meshes.length) { render.info(dim("cancelled.")); return EXIT.USER_CANCELLED; } slug = config.meshes[idx]!.slug; } const auth = getStoredToken(); const userId = auth ? getUserId(auth.session_token) : ""; const ownerCheck = auth ? await isOwner(slug, auth) : false; if (!opts.yes) { render.section(slug); if (ownerCheck) { process.stdout.write(` ${bold("1)")} remove from this device only ${dim("(keep on server)")}\n`); process.stdout.write(` ${bold("2)")} ${red("delete everywhere")} ${dim("(removes for all members)")}\n`); process.stdout.write(` ${bold("3)")} cancel\n`); render.blank(); const choice = await prompt(` ${dim("choice [1]:")} `) || "1"; if (choice === "3") { render.info(dim("cancelled.")); return EXIT.USER_CANCELLED; } if (choice === "2") { render.blank(); render.warn(`this will delete ${bold(slug)} for all members.`); const confirm = await prompt(` ${dim(`type "${slug}" to confirm:`)} `); if (confirm.toLowerCase() !== slug.toLowerCase()) { render.info(dim("cancelled.")); return EXIT.USER_CANCELLED; } try { await request({ path: `/cli/mesh/${slug}`, method: "DELETE", baseUrl: BROKER_HTTP, token: auth?.session_token, body: { user_id: userId }, }); render.ok(`deleted ${bold(slug)} from server.`); } catch (err) { render.err(`server delete failed: ${err instanceof Error ? err.message : String(err)}`); } leaveMesh(slug); render.ok("removed from local config."); return EXIT.SUCCESS; } } else { process.stdout.write(` ${bold("1)")} remove from this device ${dim("(you can re-add later)")}\n`); process.stdout.write(` ${bold("2)")} cancel\n`); if (userId) { render.blank(); render.warn("only the mesh owner can delete it from the server."); } render.blank(); const choice = await prompt(` ${dim("choice [1]:")} `) || "1"; if (choice === "2") { render.info(dim("cancelled.")); return EXIT.USER_CANCELLED; } } } const removed = leaveMesh(slug); if (removed) { render.ok(`removed ${bold(slug)} from this device.`); render.hint(`re-add anytime with: ${bold("claudemesh")} ${clay("")}`); } else { render.err(`mesh "${slug}" not found in local config.`); } return EXIT.SUCCESS; }