mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
feat(onboarding): folder name follows the project name, and project ids are UUIDs (#89)
The paste prompt now carries the project's name so an agent recommends a folder of that name; with no project at all the recommendation is `shared/` (and `bdrive init shared` names the new project after the folder), replacing the old `wiki/` default. New project ids are UUIDs instead of `p-` + 8 hex chars. The route validator still accepts the legacy shape — ids are permanent — and the client-side URL parsers (remote/http.go, bdrive share) now only check the shape of a URL segment, leaving the hub as the single authority on which ids are valid. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fb6ce347c4
commit
fd4f5c7964
@@ -28,6 +28,8 @@ test("guide: one paste for every agent, one line of prose, details collapsed", a
|
||||
"Follow https://raw.githubusercontent.com/runbear-io/beardrive/main/INSTALL_FOR_AGENTS.md",
|
||||
);
|
||||
expect(prompt).toContain(`project ${pid} on http://localhost:8993`);
|
||||
// The name rides along so the agent can recommend it as the folder name.
|
||||
expect(prompt).toContain('the project is named "wiki"');
|
||||
expect(prompt).not.toContain("brew install");
|
||||
// Detail lives behind the two collapsed sections.
|
||||
await expect(page.locator(".gd-manual > summary")).toHaveText([
|
||||
|
||||
@@ -25,7 +25,7 @@ test("deep link to a project resolves after reload", async ({ page }) => {
|
||||
test("unknown project id falls back to a real project", async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto("/p-00000000");
|
||||
await page.waitForURL(/\/p-[0-9a-f]{8}$/);
|
||||
await page.waitForURL(/\/[0-9a-f-]{36}$/);
|
||||
await expect(page.locator("#project-select")).toContainText(/.+/);
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ test("join link accepts an invite after sign-in", async ({ page, browser }) => {
|
||||
await p2.fill('input[name="password"]', PASSWORD);
|
||||
await p2.click("form button");
|
||||
await expectToast(p2, "you joined");
|
||||
await p2.waitForURL(/\/p-[0-9a-f]{8}$/); // lands on the org's project
|
||||
await p2.waitForURL(/\/[0-9a-f-]{36}$/); // lands on the org's project
|
||||
await ctx.close();
|
||||
});
|
||||
|
||||
@@ -91,13 +91,13 @@ test("new project via the sidebar + modal", async ({ page }) => {
|
||||
await page.click("#projects .nav-add");
|
||||
await page.fill(".modal-input", "scratch");
|
||||
await page.click(".modal .pbtn");
|
||||
await page.waitForURL(/\/p-[0-9a-f]{8}$/);
|
||||
await page.waitForURL(/\/[0-9a-f-]{36}$/);
|
||||
await expect(page.locator("#project-select")).toContainText("scratch");
|
||||
// Open the switcher: both projects listed; picking one navigates.
|
||||
await page.click("#project-select");
|
||||
await expect(page.getByRole("option", { name: "wiki" })).toBeVisible();
|
||||
await page.getByRole("option", { name: "wiki" }).click();
|
||||
await page.waitForURL(/\/p-[0-9a-f]{8}$/);
|
||||
await page.waitForURL(/\/[0-9a-f-]{36}$/);
|
||||
await expect(page.locator("#project-select")).toContainText("wiki");
|
||||
await expectToast(page, "Created");
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@ import { projColor } from "./ProjectNav";
|
||||
|
||||
/* ---- project home guide ----
|
||||
One paste sets up any coding agent: the prompt points at the canonical
|
||||
INSTALL_FOR_AGENTS.md with this hub's URL and this project's id filled
|
||||
in. The agent fetches the doc and handles every deviation — already
|
||||
INSTALL_FOR_AGENTS.md with this hub's URL and this project's id and
|
||||
name filled in (the name is what the agent recommends as the folder
|
||||
name). The agent fetches the doc and handles every deviation — already
|
||||
installed, no Homebrew, sign-in, wrong folder — so the page itself
|
||||
stays to one line of prose; detail lives in the collapsed sections. */
|
||||
|
||||
@@ -22,7 +23,9 @@ export function ConnectGuide({ project }: { project: Project }) {
|
||||
project.id +
|
||||
" on " +
|
||||
origin +
|
||||
". Ask me which folder to sync.";
|
||||
'. Ask me which folder to sync (the project is named "' +
|
||||
project.name +
|
||||
'").';
|
||||
const manual =
|
||||
"brew install runbear-io/tap/beardrive" +
|
||||
"\nbdrive login " +
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface Route {
|
||||
org?: string;
|
||||
// Billing (managed hubs) is hub-level like the org route; the URL comes
|
||||
// from /api/config's billing block. Reserved only in hub mode — project
|
||||
// ids are p-… so the segment can't collide with a project.
|
||||
// ids are UUIDs (or legacy p-…), so the segment can't collide with one.
|
||||
billing?: boolean;
|
||||
project?: string;
|
||||
path: string;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package webapp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -10,6 +8,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Project is one synced project hosted by this server. Its storage lives
|
||||
@@ -41,7 +41,12 @@ func (p Project) level() string {
|
||||
return p.Default
|
||||
}
|
||||
|
||||
var projectIDRe = regexp.MustCompile(`^p-[0-9a-f]{8}$`)
|
||||
// projectIDRe is the authority on what a project id may look like: a UUID
|
||||
// (what new projects get) or the legacy `p-xxxxxxxx` form hubs minted before
|
||||
// — ids are permanent, so the old shape stays valid forever. Client-side
|
||||
// parsers (remote/http.go, the share command) only check the loose shape and
|
||||
// let the hub decide.
|
||||
var projectIDRe = regexp.MustCompile(`^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|p-[0-9a-f]{8})$`)
|
||||
|
||||
// iconRe validates the *shape* of an icon name only. The list of icons a
|
||||
// project may pick from lives in the frontend (shell.tsx's PROJECT_ICONS) —
|
||||
@@ -122,11 +127,7 @@ func (db *ProjectDB) GetOrCreate(name, org string) (Project, bool, error) {
|
||||
return p, false, nil
|
||||
}
|
||||
}
|
||||
var buf [4]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return Project{}, false, err
|
||||
}
|
||||
p := Project{ID: "p-" + hex.EncodeToString(buf[:]), Name: name, Org: org, Created: time.Now().UTC()}
|
||||
p := Project{ID: uuid.NewString(), Name: name, Org: org, Created: time.Now().UTC()}
|
||||
db.byID[p.ID] = p
|
||||
if err := db.repo.Put(p); err != nil {
|
||||
delete(db.byID, p.ID)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BearDrive</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-BCH1-K2y.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D89jvKKO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ozNOEdCg.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/runbear-io/beardrive/internal/remote"
|
||||
)
|
||||
|
||||
@@ -56,7 +57,10 @@ func TestProjectAPI(t *testing.T) {
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !out.Created || out.Project.Name != "my-app" || !strings.HasPrefix(out.Project.ID, "p-") {
|
||||
// New ids are UUIDs (and must satisfy the route validator, which also
|
||||
// still accepts the legacy p-xxxxxxxx ids old hubs minted).
|
||||
if !out.Created || out.Project.Name != "my-app" ||
|
||||
uuid.Validate(out.Project.ID) != nil || !projectIDRe.MatchString(out.Project.ID) {
|
||||
t.Fatalf("create = %+v", out)
|
||||
}
|
||||
id := out.Project.ID
|
||||
|
||||
Reference in New Issue
Block a user