Skip to content
Open
26 changes: 23 additions & 3 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ const config = {
migrated.
`,
options: {
repo: { type: "string", short: "r", description: "Repository name" },
repo: { type: "string", short: "r", description: "Existing repository to connect to" },
name: {
type: "string",
short: "n",
description: "Name for a new repository (used as the domain). Required unless --repo is provided",
},
"display-name": {
type: "string",
short: "d",
description: "Display name for the new repository (defaults to --name)",
},
"no-browser": {
type: "boolean",
description: "Skip opening the browser automatically during login",
Expand All @@ -43,7 +53,12 @@ const config = {
} satisfies CommandConfig;

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

// Check for existing prismic.config.json
try {
Expand Down Expand Up @@ -114,7 +129,12 @@ export default createCommand(config, async ({ values }) => {
const adapter = await getAdapter();

if (!repo) {
repo = await createRepo({ token, host });
if (!name) {
throw new CommandError(
"Missing repository. Provide --repo to connect to an existing repository, or --name to create a new one.",
);
}
repo = await createRepo({ name, displayName, token, host });
console.info(`Created repository: ${repo}`);
}

Expand Down
61 changes: 32 additions & 29 deletions src/commands/repo-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,65 @@ import { getHost, getToken } from "../auth";
import { checkIsDomainAvailable, createRepository } from "../clients/wroom";
import { CommandError, createCommand, type CommandConfig } from "../lib/command";
import { UnknownRequestError } from "../lib/request";

const MAX_DOMAIN_TRIES = 5;
import { validateRepositoryDomain } from "../lib/repositoryDomain";

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;

validateRepositoryDomain(name);
Comment thread
jomifepe marked this conversation as resolved.
Outdated

const domain = await findAvailableDomain({ token, host });
if (!domain) {
throw new CommandError("Failed to create a repository. Please try again.");
const available = await checkIsDomainAvailable({ domain: name, token, host });
if (!available) {
throw new CommandError(
`Repository name "${name}" 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,
name: displayName ?? name,
framework,
token,
host,
});
} catch (error) {
if (error instanceof UnknownRequestError) {
const message = await error.text();
Expand All @@ -50,22 +70,5 @@ export async function createRepo(config: {
throw error;
}

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;
return name;
}
13 changes: 13 additions & 0 deletions src/lib/repositoryDomain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CommandError } from "./command";

const DOMAIN_REGEX = /^[a-z0-9][-a-z0-9]{2,}[a-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, lowercase letters/numbers/hyphens only, and start and end with a letter or number.`,
);
}
}
18 changes: 13 additions & 5 deletions test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,30 @@ 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 from --name if --repo is not provided", async ({
expect,
project,
prismic,
}) => {
await rm(new URL("prismic.config.json", project));
const { exitCode, stdout } = await prismic("init");
const name = `cli-test-${crypto.randomUUID().slice(0, 8)}`;
const { exitCode, stdout } = await prismic("init", ["--name", 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 neither --repo nor --name is 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 repository");
});

it("initializes a project with --repo when logged in", async ({
expect,
project,
Expand Down
66 changes: 49 additions & 17 deletions test/repo-create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,63 @@ 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}`);

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(name);
});

const repository = await getRepository({ repo: domain!, token, host });
expect(repository).toBeDefined();
it("rejects a non-kebab-case --name", 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("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("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");
});
Loading