Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
aeb446a
fix: use --name as repository domain in repo create and init
jomifepe May 7, 2026
ba8a947
refactor: require --name to be kebab-case instead of auto-formatting
jomifepe May 7, 2026
ad84956
refactor: align domain regex with Wroom/Dashboard
jomifepe May 7, 2026
6607b4f
fix: lowercase --name before submitting to match server behavior
jomifepe May 7, 2026
a371fcc
refactor: remove --name from init, have --repo create if missing
jomifepe May 7, 2026
ec80744
refactor: minimize init changes for new --repo-creates-if-missing flow
jomifepe May 7, 2026
b65a47a
refactor: inline validateRepositoryDomain into repo-create.ts
jomifepe May 7, 2026
10dd57b
feat: use zod schema
jomifepe May 7, 2026
525298c
fix: test
jomifepe May 7, 2026
4d1e26a
fix: validate --repo
jomifepe May 7, 2026
b163f83
refactor: move code
jomifepe May 7, 2026
08566de
feat: logging
jomifepe May 8, 2026
e0b7557
feat: logging
jomifepe May 8, 2026
8c64c1d
Apply suggestions from code review
jomifepe May 8, 2026
bef603a
refactor: redundant check
jomifepe May 8, 2026
6019802
refactor: rename
jomifepe May 8, 2026
913749f
refactor: cleanup
jomifepe May 8, 2026
51aabe9
test: max
jomifepe May 8, 2026
2d36ecd
refactor: remove casting
jomifepe May 8, 2026
d088ee3
Merge remote-tracking branch 'origin/main' into jp/repo-display-name
jomifepe May 8, 2026
e28f384
fix: call order
jomifepe May 8, 2026
2896d8f
test: move cleanup
jomifepe May 8, 2026
4cd5804
feat: match dashboard behavior omitting name
jomifepe May 8, 2026
89c8bd4
test: lowercase conversion
jomifepe May 8, 2026
428aab2
refactor: cleanup
jomifepe May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getAdapter } from "../adapters";
import { createLoginSession, getHost, getToken } from "../auth";
import { getCustomTypes, getSlices } from "../clients/custom-types";
import { getProfile } from "../clients/user";
import { checkIsDomainAvailable } from "../clients/wroom";
import { DEFAULT_PRISMIC_HOST } from "../env";
import { openBrowser } from "../lib/browser";
import { CommandError, createCommand, type CommandConfig } from "../lib/command";
Expand Down Expand Up @@ -34,7 +35,7 @@ const config = {
migrated.
`,
options: {
repo: { type: "string", short: "r", description: "Repository name" },
repo: { type: "string", short: "r", description: "Repository name (created if it doesn't exist)" },
"no-browser": {
type: "boolean",
description: "Skip opening the browser automatically during login",
Expand Down Expand Up @@ -97,24 +98,33 @@ export default createCommand(config, async ({ values }) => {
}

let repo = explicitRepo ?? legacySliceMachineConfig?.repositoryName;
let hasRepoAccess = false;
if (repo) {
const hasRepoAccess = profile.repositories.some((repository) => repository.domain === repo);
hasRepoAccess = profile.repositories.some((repository) => repository.domain === repo);
if (!hasRepoAccess) {
throw new CommandError(
`Repository "${repo}" not found in your account. Check the name or request access to the repository.`,
);
}

const isTypeBuilderEnabled = await checkIsTypeBuilderEnabled(repo, { token, host });
if (!isTypeBuilderEnabled) {
throw new TypeBuilderRequiredError();
const available = await checkIsDomainAvailable({ domain: repo, token, host });
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
if (!available) {
throw new CommandError(
`Repository "${repo}" is not in your account. Request access, or choose a different name.`,
);
}
Comment thread
jomifepe marked this conversation as resolved.
Outdated
} else {
const isTypeBuilderEnabled = await checkIsTypeBuilderEnabled(repo, { token, host });
if (!isTypeBuilderEnabled) {
throw new TypeBuilderRequiredError();
}
}
}

const adapter = await getAdapter();

if (!repo) {
repo = await createRepo({ token, host });
throw new CommandError(
"Missing --repo. Provide the repository to connect to (or to create if it doesn't exist).",
);
Comment thread
jomifepe marked this conversation as resolved.
}
if (!hasRepoAccess) {
repo = await createRepo({ name: repo, token, host });
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
console.info(`Created repository: ${repo}`);
}

Expand Down
69 changes: 42 additions & 27 deletions src/commands/repo-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,76 @@ import { checkIsDomainAvailable, createRepository } from "../clients/wroom";
import { CommandError, createCommand, type CommandConfig } from "../lib/command";
import { UnknownRequestError } from "../lib/request";

const MAX_DOMAIN_TRIES = 5;
const DOMAIN_REGEX = /^[a-zA-Z0-9][-a-zA-Z0-9]{2,}[a-zA-Z0-9]$/;
const MIN_LENGTH = 4;
const MAX_LENGTH = 63;

export function validateRepositoryDomain(name: string): void {
if (name.length < MIN_LENGTH || name.length > MAX_LENGTH || !DOMAIN_REGEX.test(name)) {
throw new CommandError(
`Invalid repository name "${name}". Must be ${MIN_LENGTH}–${MAX_LENGTH} characters, letters/numbers/hyphens only, and start and end with a letter or number.`,
);
}
}

const config = {
name: "prismic repo create",
description: "Create a new Prismic repository.",
options: {
name: { type: "string", short: "n", description: "Display name for the repository" },
name: {
type: "string",
short: "n",
description: "Repository name (used as the domain)",
required: true,
},
"display-name": {
type: "string",
short: "d",
description: "Display name for the repository (defaults to --name)",
},
},
} satisfies CommandConfig;

export default createCommand(config, async ({ values }) => {
const { name } = values;
const { name, "display-name": displayName } = values;

const token = await getToken();
const host = await getHost();
const domain = await createRepo({ name, token, host });
const domain = await createRepo({ name, displayName, token, host });

console.info(`Repository created: ${domain}`);
console.info(`URL: https://${domain}.${host}/`);
});

export async function createRepo(config: {
name?: string;
name: string;
displayName?: string;
token: string | undefined;
host: string;
}): Promise<string> {
const { name, token, host } = config;
const { name, displayName, token, host } = config;

const domain = await findAvailableDomain({ token, host });
if (!domain) {
throw new CommandError("Failed to create a repository. Please try again.");
validateRepositoryDomain(name);
Comment thread
jomifepe marked this conversation as resolved.
Outdated
const domain = name.toLowerCase();

const available = await checkIsDomainAvailable({ domain, token, host });
if (!available) {
throw new CommandError(
`Repository name "${domain}" is already taken. Choose another.`,
);
}

const adapter = await getAdapter().catch(() => undefined);
const framework = adapter?.id ?? "other";

try {
await createRepository({ domain, name: name ?? domain, framework, token, host });
await createRepository({
domain,
name: displayName ?? name,
framework,
token,
host,
});
} catch (error) {
if (error instanceof UnknownRequestError) {
const message = await error.text();
Expand All @@ -52,20 +84,3 @@ export async function createRepo(config: {

return domain;
}

async function findAvailableDomain(config: {
token: string | undefined;
host: string;
}): Promise<string | undefined> {
const { token, host } = config;
let domain;
for (let i = 0; i < MAX_DOMAIN_TRIES; i++) {
const candidate = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
const available = await checkIsDomainAvailable({ domain: candidate, token, host });
if (available) {
domain = candidate;
break;
}
}
return domain;
}
33 changes: 25 additions & 8 deletions test/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { access, readFile, rm, writeFile } from "node:fs/promises";

import { onTestFinished } from "vitest";

import { captureOutput, it } from "./it";
import { deleteRepository } from "./prismic";

it("supports --help", async ({ expect, prismic }) => {
const { stdout, exitCode } = await prismic("init", ["--help"]);
Expand All @@ -14,22 +17,35 @@ it("fails if prismic.config.json already exists", async ({ expect, prismic }) =>
expect(stderr).toContain("already initialized");
});

it("creates a repo if --repo is not provided and no legacy config exists", async ({
it("creates a repo when --repo doesn't exist yet", async ({
expect,
project,
prismic,
token,
host,
password,
}) => {
await rm(new URL("prismic.config.json", project));
const { exitCode, stdout } = await prismic("init");
const name = `cli-test-${crypto.randomUUID().slice(0, 8)}`;
onTestFinished(() => deleteRepository(name, { token, password, host }));

const { exitCode, stdout } = await prismic("init", ["--repo", name]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Created repository:");
expect(stdout).toContain("Initialized Prismic for repository");
expect(stdout).toContain(`Created repository: ${name}`);
expect(stdout).toContain(`Initialized Prismic for repository "${name}"`);

const configRaw = await readFile(new URL("prismic.config.json", project), "utf-8");
const config = JSON.parse(configRaw);
expect(config.repositoryName).toMatch(/^[a-f0-9]{8}$/);
expect(config.repositoryName).toBe(name);
}, 60_000);

it("fails when --repo is not provided", async ({ expect, project, prismic }) => {
await rm(new URL("prismic.config.json", project));
const { exitCode, stderr } = await prismic("init");
expect(exitCode).toBe(1);
expect(stderr).toContain("Missing --repo");
});

it("initializes a project with --repo when logged in", async ({
expect,
project,
Expand Down Expand Up @@ -59,11 +75,12 @@ it("triggers login flow when not logged in", async ({ expect, project, prismic,
proc.kill();
});

it("fails if repo is not in the user's account", async ({ expect, project, prismic }) => {
it("fails if --repo is taken by another account", async ({ expect, project, prismic }) => {
await rm(new URL("prismic.config.json", project));
const { exitCode, stderr } = await prismic("init", ["--repo", "nonexistent-repo-xyz-12345"]);
// "prismic" is reserved/taken and will fail availability check.
const { exitCode, stderr } = await prismic("init", ["--repo", "prismic"]);
expect(exitCode).toBe(1);
expect(stderr).toContain("not found in your account");
expect(stderr).toContain("not in your account");
});

it("migrates slicemachine.config.json", async ({ expect, project, prismic, repo }) => {
Expand Down
84 changes: 68 additions & 16 deletions test/repo-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,83 @@ it("supports --help", async ({ expect, prismic }) => {
expect(stdout).toContain("prismic repo create [options]");
});

it("creates a repository", async ({ expect, prismic, token, host, password }) => {
const { stdout, exitCode } = await prismic("repo", ["create"]);
it("requires --name", async ({ expect, prismic }) => {
const { stderr, exitCode } = await prismic("repo", ["create"]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Missing required option: --name");
});

it("creates a repository using --name as the domain", async ({
expect,
prismic,
token,
host,
password,
}) => {
const name = `cli-test-${crypto.randomUUID().slice(0, 8)}`;
const { stdout, exitCode } = await prismic("repo", ["create", "--name", name]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Repository created:");
expect(stdout).toContain(`Repository created: ${name}`);

onTestFinished(() => deleteRepository(name, { token, password, host }));
Comment thread
jomifepe marked this conversation as resolved.
Outdated

const repository = await getRepository({ repo: name, token, host });
expect(repository.name).toBe(name);
});

const domain = stdout.match(/Repository created: (\S+)/)?.[1];
expect(domain).toBeDefined();
it("lowercases --name before submitting", async ({
expect,
prismic,
token,
host,
password,
}) => {
const suffix = crypto.randomUUID().slice(0, 8);
const name = `CLI-Test-${suffix}`;
const expectedDomain = name.toLowerCase();
const { stdout, exitCode } = await prismic("repo", ["create", "--name", name]);
expect(exitCode).toBe(0);
expect(stdout).toContain(`Repository created: ${expectedDomain}`);

onTestFinished(() => deleteRepository(domain!, { token, password, host }));
onTestFinished(() => deleteRepository(expectedDomain, { token, password, host }));

const repository = await getRepository({ repo: domain!, token, host });
const repository = await getRepository({ repo: expectedDomain, token, host });
expect(repository).toBeDefined();
});

it("creates a repository with a name", async ({ expect, prismic, token, host, password }) => {
const name = `Test ${crypto.randomUUID().slice(0, 8)}`;
const { stdout, exitCode } = await prismic("repo", ["create", "--name", name]);
it("rejects --name with disallowed characters", async ({ expect, prismic }) => {
const { stderr, exitCode } = await prismic("repo", ["create", "--name", "My Test Repo"]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Invalid repository name");
});

it("uses --display-name as the repository label", async ({
expect,
prismic,
token,
host,
password,
}) => {
const name = `cli-test-${crypto.randomUUID().slice(0, 8)}`;
const displayName = "My Display Name";
const { stdout, exitCode } = await prismic("repo", [
"create",
"--name",
name,
"--display-name",
displayName,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Repository created:");
expect(stdout).toContain(`Repository created: ${name}`);

const domain = stdout.match(/Repository created: (\S+)/)?.[1];
expect(domain).toBeDefined();
onTestFinished(() => deleteRepository(name, { token, password, host }));

onTestFinished(() => deleteRepository(domain!, { token, password, host }));
const repository = await getRepository({ repo: name, token, host });
expect(repository.name).toBe(displayName);
});

const repository = await getRepository({ repo: domain!, token, host });
expect(repository.name).toBe(name);
it("fails with guidance when --name is invalid", async ({ expect, prismic }) => {
const { stderr, exitCode } = await prismic("repo", ["create", "--name", "!!"]);
expect(exitCode).toBe(1);
expect(stderr).toContain("Invalid repository name");
});