feat: collapse mesh.name and mesh.slug into one identifier (v1.21.0)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Pre-launch fix: every visible surface already keyed on slug, so
"name" was a parallel string that only existed to confuse users
on rename ("I renamed but nothing visible changed").

Now slug IS the identifier. claudemesh rename <old> <new> is the
whole rename surface. PATCH /api/cli/meshes/:slug body becomes
{ slug } and the route writes both columns to keep them in sync.
Mesh create derives slug from input.name and stores name = slug.
Pickers drop the (parens). The claudemesh slug verb shipped 30
min ago is removed — merged into rename.

The mesh.name DB column stays for now to avoid touching ~25
reader sites; a follow-up migration drops it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-03 15:23:04 +01:00
parent 03cff156e2
commit 5785454ac9
15 changed files with 94 additions and 160 deletions

View File

@@ -79,20 +79,15 @@ export async function PATCH(
}
const { slug } = await params;
let body: { name?: string; slug?: string };
let body: { slug?: string };
try {
body = (await request.json()) as { name?: string; slug?: string };
body = (await request.json()) as { slug?: string };
} catch {
return NextResponse.json({ error: "invalid JSON body" }, { status: 400 });
}
const newName = body.name?.trim();
const newSlug = body.slug?.trim();
if (!newName && !newSlug) {
return NextResponse.json({ error: "name or slug is required" }, { status: 400 });
}
if (newName !== undefined && newName.length > 80) {
return NextResponse.json({ error: "name too long (max 80 chars)" }, { status: 400 });
if (!newSlug) {
return NextResponse.json({ error: "slug is required" }, { status: 400 });
}
// Slug regex matches the CLI's pre-flight check. Lowercase only,
// must start with alnum, may contain hyphens, 2-32 chars total.
@@ -100,7 +95,7 @@ export async function PATCH(
// comment on mesh.slug — so we don't enforce a uniqueness collision
// here. Local CLI configs key on slug, so the picker collides
// locally; that's the user's call.
if (newSlug !== undefined && !/^[a-z0-9][a-z0-9-]{1,31}$/.test(newSlug)) {
if (!/^[a-z0-9][a-z0-9-]{1,31}$/.test(newSlug)) {
return NextResponse.json(
{ error: "slug must be 2-32 chars, lowercase alnum + hyphens, start with alnum" },
{ status: 400 },
@@ -132,15 +127,14 @@ export async function PATCH(
);
}
const patch: { name?: string; slug?: string } = {};
if (newName !== undefined) patch.name = newName;
if (newSlug !== undefined) patch.slug = newSlug;
// Soft-collapse: name and slug are a single concept user-facing,
// so we always sync name = slug. mesh.name column stays for now
// (avoids touching ~25 reader sites); a future migration drops it.
const [updated] = await db
.update(mesh)
.set(patch)
.set({ slug: newSlug, name: newSlug })
.where(eq(mesh.slug, slug))
.returning({ slug: mesh.slug, name: mesh.name });
.returning({ slug: mesh.slug });
return NextResponse.json(updated);
}