@angeloashmore/prismic-cli-poc 0.0.0-canary.1d36cd8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +98 -0
  3. package/dist/index.mjs +2548 -0
  4. package/package.json +53 -0
  5. package/src/codegen-types.ts +82 -0
  6. package/src/codegen.ts +45 -0
  7. package/src/custom-type-add-field-boolean.ts +192 -0
  8. package/src/custom-type-add-field-color.ts +177 -0
  9. package/src/custom-type-add-field-date.ts +180 -0
  10. package/src/custom-type-add-field-embed.ts +177 -0
  11. package/src/custom-type-add-field-geo-point.ts +174 -0
  12. package/src/custom-type-add-field-image.ts +177 -0
  13. package/src/custom-type-add-field-key-text.ts +177 -0
  14. package/src/custom-type-add-field-link.ts +201 -0
  15. package/src/custom-type-add-field-number.ts +209 -0
  16. package/src/custom-type-add-field-rich-text.ts +202 -0
  17. package/src/custom-type-add-field-select.ts +192 -0
  18. package/src/custom-type-add-field-timestamp.ts +180 -0
  19. package/src/custom-type-add-field-uid.ts +177 -0
  20. package/src/custom-type-add-field.ts +111 -0
  21. package/src/custom-type-connect-slice.ts +220 -0
  22. package/src/custom-type-create.ts +118 -0
  23. package/src/custom-type-disconnect-slice.ts +177 -0
  24. package/src/custom-type-list.ts +110 -0
  25. package/src/custom-type-remove-field.ts +177 -0
  26. package/src/custom-type-remove.ts +144 -0
  27. package/src/custom-type-set-name.ts +144 -0
  28. package/src/custom-type-view.ts +118 -0
  29. package/src/custom-type.ts +85 -0
  30. package/src/index.ts +127 -0
  31. package/src/init.ts +64 -0
  32. package/src/lib/auth.ts +83 -0
  33. package/src/lib/config.ts +111 -0
  34. package/src/lib/custom-types-api.ts +438 -0
  35. package/src/lib/file.ts +49 -0
  36. package/src/lib/framework.ts +143 -0
  37. package/src/lib/json.ts +3 -0
  38. package/src/lib/request.ts +116 -0
  39. package/src/lib/slice.ts +115 -0
  40. package/src/lib/string.ts +6 -0
  41. package/src/lib/url.ts +25 -0
  42. package/src/locale-add.ts +116 -0
  43. package/src/locale-list.ts +107 -0
  44. package/src/locale-remove.ts +88 -0
  45. package/src/locale-set-default.ts +131 -0
  46. package/src/locale.ts +60 -0
  47. package/src/login.ts +152 -0
  48. package/src/logout.ts +36 -0
  49. package/src/page-type-add-field-boolean.ts +192 -0
  50. package/src/page-type-add-field-color.ts +177 -0
  51. package/src/page-type-add-field-date.ts +180 -0
  52. package/src/page-type-add-field-embed.ts +177 -0
  53. package/src/page-type-add-field-geo-point.ts +174 -0
  54. package/src/page-type-add-field-image.ts +177 -0
  55. package/src/page-type-add-field-key-text.ts +177 -0
  56. package/src/page-type-add-field-link.ts +201 -0
  57. package/src/page-type-add-field-number.ts +209 -0
  58. package/src/page-type-add-field-rich-text.ts +202 -0
  59. package/src/page-type-add-field-select.ts +192 -0
  60. package/src/page-type-add-field-timestamp.ts +180 -0
  61. package/src/page-type-add-field-uid.ts +177 -0
  62. package/src/page-type-add-field.ts +111 -0
  63. package/src/page-type-connect-slice.ts +220 -0
  64. package/src/page-type-create.ts +142 -0
  65. package/src/page-type-disconnect-slice.ts +177 -0
  66. package/src/page-type-list.ts +109 -0
  67. package/src/page-type-remove-field.ts +177 -0
  68. package/src/page-type-remove.ts +144 -0
  69. package/src/page-type-set-name.ts +144 -0
  70. package/src/page-type-set-repeatable.ts +153 -0
  71. package/src/page-type-view.ts +118 -0
  72. package/src/page-type.ts +90 -0
  73. package/src/preview-add.ts +126 -0
  74. package/src/preview-get-simulator.ts +104 -0
  75. package/src/preview-list.ts +106 -0
  76. package/src/preview-remove-simulator.ts +80 -0
  77. package/src/preview-remove.ts +109 -0
  78. package/src/preview-set-name.ts +137 -0
  79. package/src/preview-set-simulator.ts +116 -0
  80. package/src/preview.ts +75 -0
  81. package/src/pull.ts +247 -0
  82. package/src/push.ts +405 -0
  83. package/src/repo-create.ts +136 -0
  84. package/src/repo-get-access.ts +86 -0
  85. package/src/repo-list.ts +100 -0
  86. package/src/repo-set-access.ts +100 -0
  87. package/src/repo-set-name.ts +102 -0
  88. package/src/repo-view.ts +113 -0
  89. package/src/repo.ts +70 -0
  90. package/src/slice-add-field-boolean.ts +173 -0
  91. package/src/slice-add-field-color.ts +158 -0
  92. package/src/slice-add-field-date.ts +158 -0
  93. package/src/slice-add-field-embed.ts +158 -0
  94. package/src/slice-add-field-geo-point.ts +155 -0
  95. package/src/slice-add-field-image.ts +155 -0
  96. package/src/slice-add-field-key-text.ts +158 -0
  97. package/src/slice-add-field-link.ts +178 -0
  98. package/src/slice-add-field-number.ts +158 -0
  99. package/src/slice-add-field-rich-text.ts +183 -0
  100. package/src/slice-add-field-select.ts +173 -0
  101. package/src/slice-add-field-timestamp.ts +158 -0
  102. package/src/slice-add-field.ts +106 -0
  103. package/src/slice-add-variation.ts +145 -0
  104. package/src/slice-create.ts +148 -0
  105. package/src/slice-list-variations.ts +67 -0
  106. package/src/slice-list.ts +88 -0
  107. package/src/slice-remove-field.ts +128 -0
  108. package/src/slice-remove-variation.ts +118 -0
  109. package/src/slice-remove.ts +97 -0
  110. package/src/slice-rename.ts +128 -0
  111. package/src/slice-view.ts +77 -0
  112. package/src/slice.ts +90 -0
  113. package/src/status.ts +733 -0
  114. package/src/token-create.ts +203 -0
  115. package/src/token-delete.ts +182 -0
  116. package/src/token-list.ts +223 -0
  117. package/src/token-set-name.ts +193 -0
  118. package/src/token.ts +60 -0
  119. package/src/webhook-add-header.ts +118 -0
  120. package/src/webhook-create.ts +152 -0
  121. package/src/webhook-disable.ts +109 -0
  122. package/src/webhook-enable.ts +132 -0
  123. package/src/webhook-list.ts +93 -0
  124. package/src/webhook-remove-header.ts +117 -0
  125. package/src/webhook-remove.ts +106 -0
  126. package/src/webhook-set-triggers.ts +148 -0
  127. package/src/webhook-status.ts +90 -0
  128. package/src/webhook-test.ts +106 -0
  129. package/src/webhook-view.ts +147 -0
  130. package/src/webhook.ts +95 -0
  131. package/src/whoami.ts +62 -0
@@ -0,0 +1,143 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import * as v from "valibot";
3
+
4
+ import { exists, findUpward } from "./file";
5
+
6
+ export type Framework = "next" | "nuxt" | "sveltekit";
7
+
8
+ export type FrameworkInfo = {
9
+ framework: Framework | undefined;
10
+ hasSrcDir: boolean;
11
+ projectRoot: URL;
12
+ };
13
+
14
+ const PackageJsonSchema = v.object({
15
+ dependencies: v.optional(v.record(v.string(), v.string())),
16
+ devDependencies: v.optional(v.record(v.string(), v.string())),
17
+ });
18
+
19
+ export async function detectFrameworkInfo(): Promise<FrameworkInfo | undefined> {
20
+ const packageJsonPath = await findUpward("package.json");
21
+ if (!packageJsonPath) return undefined;
22
+
23
+ const projectRoot = new URL(".", packageJsonPath);
24
+
25
+ let framework: Framework | undefined;
26
+ try {
27
+ const contents = await readFile(packageJsonPath, "utf8");
28
+ const { dependencies = {}, devDependencies = {} } = v.parse(
29
+ PackageJsonSchema,
30
+ JSON.parse(contents),
31
+ );
32
+ const allDeps = { ...dependencies, ...devDependencies };
33
+ if ("next" in allDeps) framework = "next";
34
+ else if ("nuxt" in allDeps) framework = "nuxt";
35
+ else if ("@sveltejs/kit" in allDeps) framework = "sveltekit";
36
+ } catch {
37
+ // Continue with undefined framework
38
+ }
39
+
40
+ let hasSrcDir = false;
41
+ if (framework === "next" || framework === "sveltekit") {
42
+ hasSrcDir = await exists(new URL("src/", projectRoot));
43
+ } else if (framework === "nuxt") {
44
+ hasSrcDir = await exists(new URL("app/", projectRoot));
45
+ }
46
+
47
+ return { framework, hasSrcDir, projectRoot };
48
+ }
49
+
50
+ export function getRequiredDependencies(framework: Framework | undefined): string[] {
51
+ switch (framework) {
52
+ case "next":
53
+ return ["@prismicio/client", "@prismicio/react", "@prismicio/next"];
54
+ case "nuxt":
55
+ return ["@nuxtjs/prismic"];
56
+ case "sveltekit":
57
+ return ["@prismicio/client", "@prismicio/svelte"];
58
+ default:
59
+ return ["@prismicio/client"];
60
+ }
61
+ }
62
+
63
+ export function getClientFilePath(info: FrameworkInfo): string | null {
64
+ switch (info.framework) {
65
+ case "next":
66
+ return info.hasSrcDir ? "src/prismicio.ts" : "prismicio.ts";
67
+ case "nuxt":
68
+ return null; // Nuxt uses nuxt.config.ts instead
69
+ case "sveltekit":
70
+ return "src/lib/prismicio.ts";
71
+ default:
72
+ return "prismicio.ts";
73
+ }
74
+ }
75
+
76
+ export function getSlicesDirectory(info: FrameworkInfo): string {
77
+ switch (info.framework) {
78
+ case "next":
79
+ return info.hasSrcDir ? "src/slices/" : "slices/";
80
+ case "nuxt":
81
+ return info.hasSrcDir ? "app/slices/" : "slices/";
82
+ case "sveltekit":
83
+ return "src/lib/slices/";
84
+ default:
85
+ return "slices/";
86
+ }
87
+ }
88
+
89
+ export function getSliceComponentExtensions(framework: Framework | undefined): string[] {
90
+ switch (framework) {
91
+ case "next":
92
+ return [".tsx", ".ts", ".jsx", ".js"];
93
+ case "nuxt":
94
+ return [".vue"];
95
+ case "sveltekit":
96
+ return [".svelte"];
97
+ default:
98
+ return [".tsx", ".ts", ".jsx", ".js"];
99
+ }
100
+ }
101
+
102
+ export function getRoutePath(
103
+ info: FrameworkInfo,
104
+ route: string,
105
+ ): { path: string; extensions: string[] } | null {
106
+ switch (info.framework) {
107
+ case "next": {
108
+ const base = info.hasSrcDir ? "src/app" : "app";
109
+ if (route === "/slice-simulator") {
110
+ return { path: `${base}/slice-simulator/page`, extensions: [".tsx", ".ts", ".jsx", ".js"] };
111
+ }
112
+ if (route === "/api/preview") {
113
+ return { path: `${base}/api/preview/route`, extensions: [".ts", ".js"] };
114
+ }
115
+ if (route === "/api/exit-preview") {
116
+ return { path: `${base}/api/exit-preview/route`, extensions: [".ts", ".js"] };
117
+ }
118
+ if (route === "/api/revalidate") {
119
+ return { path: `${base}/api/revalidate/route`, extensions: [".ts", ".js"] };
120
+ }
121
+ return null;
122
+ }
123
+ case "nuxt": {
124
+ if (route === "/slice-simulator") {
125
+ return { path: "pages/slice-simulator", extensions: [".vue"] };
126
+ }
127
+ // Preview endpoints are built-in for Nuxt
128
+ return null;
129
+ }
130
+ case "sveltekit": {
131
+ if (route === "/slice-simulator") {
132
+ return { path: "src/routes/slice-simulator/+page", extensions: [".svelte"] };
133
+ }
134
+ if (route === "/api/preview") {
135
+ return { path: "src/routes/api/preview/+server", extensions: [".ts", ".js"] };
136
+ }
137
+ // exit-preview not required for SvelteKit
138
+ return null;
139
+ }
140
+ default:
141
+ return null;
142
+ }
143
+ }
@@ -0,0 +1,3 @@
1
+ export function stringify(input: unknown): string {
2
+ return JSON.stringify(input, null, 2);
3
+ }
@@ -0,0 +1,116 @@
1
+ import * as v from "valibot";
2
+
3
+ import { readToken } from "./auth";
4
+
5
+ type CustomRequestInit = Omit<RequestInit, "body"> & {
6
+ body?: RequestInit["body"] | Record<PropertyKey, unknown>;
7
+ };
8
+
9
+ export type RequestResponse<T> = SuccessfulRequestResponse<T> | FailedRequestResponse;
10
+ export type ParsedRequestResponse<T> =
11
+ | RequestResponse<T>
12
+ | { ok: false; value: unknown; error: v.ValiError<v.GenericSchema<T>> };
13
+ export type SuccessfulRequestResponse<T> = { ok: true; value: T };
14
+ export type FailedRequestResponse = {
15
+ ok: false;
16
+ value: unknown;
17
+ error: RequestError | ForbiddenRequestError | UnauthorizedRequestError;
18
+ };
19
+ export type FailedParsedRequestResponse<T> =
20
+ | FailedRequestResponse
21
+ | { ok: false; value: unknown; error: v.ValiError<v.GenericSchema<T>> };
22
+
23
+ export async function request<T>(
24
+ input: RequestInfo | URL,
25
+ init?: CustomRequestInit,
26
+ ): Promise<RequestResponse<T>>;
27
+ export async function request<T>(
28
+ input: RequestInfo | URL,
29
+ init: CustomRequestInit & { schema: v.GenericSchema<T> },
30
+ ): Promise<ParsedRequestResponse<T>>;
31
+ export async function request<T>(
32
+ input: RequestInfo | URL,
33
+ init: CustomRequestInit & { schema?: v.GenericSchema<T> } = {},
34
+ ): Promise<ParsedRequestResponse<T>> {
35
+ const { credentials = "include" } = init;
36
+
37
+ const headers = new Headers(init.headers);
38
+ headers.set("Accept", "application/json");
39
+ if (credentials === "include") {
40
+ const token = await readToken();
41
+ if (token) headers.set("Cookie", `SESSION=fake_session; prismic-auth=${token}`);
42
+ }
43
+ if (!headers.has("Content-Type") && init.body) {
44
+ headers.set("Content-Type", "application/json");
45
+ }
46
+ if (init.body instanceof FormData) {
47
+ headers.delete("Content-Type");
48
+ }
49
+
50
+ const body =
51
+ headers.get("Content-Type") === "application/json"
52
+ ? JSON.stringify(init.body)
53
+ : (init.body as RequestInit["body"]);
54
+
55
+ const response = await fetch(input, { ...init, body, headers });
56
+
57
+ const value = await response
58
+ .clone()
59
+ .json()
60
+ .catch(() => response.clone().text());
61
+
62
+ if (response.ok) {
63
+ if (!init?.schema) return { ok: true, value };
64
+
65
+ try {
66
+ const parsed = v.parse(init.schema, value);
67
+ return { ok: true, value: parsed };
68
+ } catch (error) {
69
+ if (v.isValiError<v.GenericSchema<T>>(error)) {
70
+ return { ok: false, value, error };
71
+ }
72
+ throw error;
73
+ }
74
+ } else {
75
+ if (response.status === 401) {
76
+ const error = new UnauthorizedRequestError(response);
77
+ return { ok: false, value, error };
78
+ } else if (response.status === 403) {
79
+ const error = new ForbiddenRequestError(response);
80
+ return { ok: false, value, error };
81
+ } else {
82
+ const error = new RequestError(response);
83
+ return { ok: false, value, error };
84
+ }
85
+ }
86
+ }
87
+
88
+ export class RequestError extends Error {
89
+ name = "RequestError" as const;
90
+ response: Response;
91
+
92
+ constructor(response: Response) {
93
+ super(`fetch failed: ${response.url}`);
94
+ this.response = response;
95
+ }
96
+
97
+ async text(): Promise<string> {
98
+ return this.response.clone().text();
99
+ }
100
+
101
+ async json(): Promise<unknown> {
102
+ return this.response.clone().json();
103
+ }
104
+
105
+ get status(): number {
106
+ return this.response.status;
107
+ }
108
+
109
+ get statusText(): string {
110
+ return this.response.statusText;
111
+ }
112
+ }
113
+
114
+ export class ForbiddenRequestError extends RequestError {}
115
+
116
+ export class UnauthorizedRequestError extends RequestError {}
@@ -0,0 +1,115 @@
1
+ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import * as v from "valibot";
5
+
6
+ import { exists, findUpward } from "./file";
7
+
8
+ export const SharedSliceSchema = v.object({
9
+ id: v.string(),
10
+ type: v.literal("SharedSlice"),
11
+ name: v.string(),
12
+ description: v.optional(v.string()),
13
+ variations: v.array(
14
+ v.object({
15
+ id: v.string(),
16
+ name: v.string(),
17
+ description: v.optional(v.string()),
18
+ docURL: v.optional(v.string()),
19
+ version: v.optional(v.string()),
20
+ imageUrl: v.optional(v.string()),
21
+ primary: v.optional(v.record(v.string(), v.unknown())),
22
+ items: v.optional(v.record(v.string(), v.unknown())),
23
+ }),
24
+ ),
25
+ });
26
+
27
+ type SliceModelResult =
28
+ | { ok: true; model: SharedSlice; modelPath: URL }
29
+ | { ok: false; error: string };
30
+
31
+ export async function findSliceModel(sliceId: string): Promise<SliceModelResult> {
32
+ const projectRoot = await findUpward("package.json");
33
+ if (!projectRoot) {
34
+ return { ok: false, error: "Could not find project root (no package.json found)" };
35
+ }
36
+
37
+ const slicesDirectory = await getSlicesDirectory();
38
+
39
+ // List all directories in slices folder
40
+ let entries: string[];
41
+ try {
42
+ entries = (await readdir(slicesDirectory, { withFileTypes: false })) as unknown as string[];
43
+ } catch {
44
+ return { ok: false, error: `No slices directory found at ${slicesDirectory.href}` };
45
+ }
46
+
47
+ // Search for a slice with matching ID
48
+ for (const entry of entries) {
49
+ const modelPath = new URL(`${entry}/model.json`, slicesDirectory);
50
+ try {
51
+ const contents = await readFile(modelPath, "utf8");
52
+ const parsed = JSON.parse(contents);
53
+ if (parsed.id === sliceId) {
54
+ const result = v.safeParse(SharedSliceSchema, parsed);
55
+ if (!result.success) {
56
+ return { ok: false, error: `Invalid slice model at ${modelPath.href}` };
57
+ }
58
+ return { ok: true, model: result.output as SharedSlice, modelPath };
59
+ }
60
+ } catch {
61
+ // Skip directories without valid model.json
62
+ }
63
+ }
64
+
65
+ return {
66
+ ok: false,
67
+ error: `Slice not found: ${sliceId}\n\nCreate it first with: prismic slice create ${sliceId}`,
68
+ };
69
+ }
70
+
71
+ export async function getSlicesDirectory(): Promise<URL> {
72
+ const framework = await detectFramework();
73
+ const projectRoot = await findUpward("package.json");
74
+ if (!projectRoot) {
75
+ throw new Error("Could not find project root (no package.json found)");
76
+ }
77
+ const projectDir = new URL(".", projectRoot);
78
+
79
+ switch (framework) {
80
+ case "next": {
81
+ const hasSrcDir = await exists(new URL("src", projectDir));
82
+ if (hasSrcDir) return new URL("src/slices/", projectDir);
83
+ }
84
+ case "nuxt": {
85
+ const hasAppDir = await exists(new URL("app", projectDir));
86
+ if (hasAppDir) return new URL("app/slices/", projectDir);
87
+ }
88
+ case "sveltekit": {
89
+ return new URL("src/slices/", projectDir);
90
+ }
91
+ }
92
+ return new URL("slices/", projectDir);
93
+ }
94
+
95
+ const PackageJsonSchema = v.object({
96
+ dependencies: v.optional(v.record(v.string(), v.string())),
97
+ });
98
+
99
+ type Framework = "next" | "nuxt" | "sveltekit";
100
+
101
+ async function detectFramework(): Promise<Framework | undefined> {
102
+ const packageJsonPath = await findUpward("package.json");
103
+ if (!packageJsonPath) return;
104
+ try {
105
+ const contents = await readFile(packageJsonPath, "utf8");
106
+ const { dependencies = {} } = v.parse(PackageJsonSchema, JSON.parse(contents));
107
+ if ("next" in dependencies) return "next";
108
+ if ("nuxt" in dependencies) return "nuxt";
109
+ if ("@sveltejs/kit" in dependencies) return "sveltekit";
110
+ } catch {}
111
+ }
112
+
113
+ export function pascalCase(input: string): string {
114
+ return input.toLowerCase().replace(/(^|[-_\s]+)(.)?/g, (_, __, c) => c?.toUpperCase() ?? "");
115
+ }
@@ -0,0 +1,6 @@
1
+ export function humanReadable(id: string): string {
2
+ return id
3
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
4
+ .replace(/[_-]+/g, " ")
5
+ .replace(/\b\w/g, (c) => c.toUpperCase());
6
+ }
package/src/lib/url.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { readHost } from "./auth";
2
+
3
+ export async function getRepoUrl(repo: string): Promise<URL> {
4
+ const host = await readHost();
5
+ host.hostname = `${repo}.${host.hostname}`;
6
+ return appendTrailingSlash(host);
7
+ }
8
+
9
+ export async function getInternalApiUrl(): Promise<URL> {
10
+ const host = await readHost();
11
+ host.hostname = `api.internal.${host.hostname}`;
12
+ return appendTrailingSlash(host);
13
+ }
14
+
15
+ export async function getUserServiceUrl(): Promise<URL> {
16
+ const host = await readHost();
17
+ host.hostname = `user-service.${host.hostname}`;
18
+ return appendTrailingSlash(host);
19
+ }
20
+
21
+ export function appendTrailingSlash(url: string | URL): URL {
22
+ const newURL = new URL(url);
23
+ if (!newURL.pathname.endsWith("/")) newURL.pathname += "/";
24
+ return newURL;
25
+ }
@@ -0,0 +1,116 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ import { isAuthenticated } from "./lib/auth";
4
+ import { safeGetRepositoryFromConfig } from "./lib/config";
5
+ import { stringify } from "./lib/json";
6
+ import { ForbiddenRequestError, request } from "./lib/request";
7
+ import { getRepoUrl } from "./lib/url";
8
+
9
+ const HELP = `
10
+ Add a new locale to a Prismic repository.
11
+
12
+ By default, this command reads the repository from prismic.config.json at the
13
+ project root.
14
+
15
+ USAGE
16
+ prismic locale add <code> [flags]
17
+
18
+ ARGUMENTS
19
+ <code> Locale code (e.g. fr-fr, es-es)
20
+
21
+ FLAGS
22
+ -n, --name string Custom display name (creates custom locale)
23
+ -r, --repo string Repository domain
24
+ -h, --help Show help for command
25
+
26
+ LEARN MORE
27
+ Use \`prismic <command> <subcommand> --help\` for more information about a command.
28
+ `.trim();
29
+
30
+ export async function localeAdd(): Promise<void> {
31
+ const {
32
+ values: { help, name, repo = await safeGetRepositoryFromConfig() },
33
+ positionals: [code],
34
+ } = parseArgs({
35
+ args: process.argv.slice(4), // skip: node, script, "locale", "add"
36
+ options: {
37
+ name: { type: "string", short: "n" },
38
+ repo: { type: "string", short: "r" },
39
+ help: { type: "boolean", short: "h" },
40
+ },
41
+ allowPositionals: true,
42
+ });
43
+
44
+ if (help) {
45
+ console.info(HELP);
46
+ return;
47
+ }
48
+
49
+ if (!code) {
50
+ console.error("Missing required argument: <code>");
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+
55
+ if (!repo) {
56
+ console.error("Missing prismic.config.json or --repo option");
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ const authenticated = await isAuthenticated();
62
+ if (!authenticated) {
63
+ handleUnauthenticated();
64
+
65
+ return;
66
+ }
67
+
68
+ const response = name
69
+ ? await addCustomLocale(repo, code, name)
70
+ : await addStandardLocale(repo, code);
71
+ if (!response.ok) {
72
+ if (
73
+ typeof response.value === "string" &&
74
+ response.value.includes("already existing languages")
75
+ ) {
76
+ // Treat as success
77
+ return;
78
+ }
79
+
80
+ if (response.error instanceof ForbiddenRequestError) {
81
+ handleUnauthenticated();
82
+ } else {
83
+ console.error(`Failed to add locale: ${stringify(response.value)}`);
84
+ process.exitCode = 1;
85
+ }
86
+
87
+ return;
88
+ }
89
+
90
+ console.info(`Locale added: ${code}`);
91
+ }
92
+
93
+ async function addStandardLocale(repo: string, code: string) {
94
+ const url = new URL("/app/settings/multilanguages", await getRepoUrl(repo));
95
+ return await request(url, {
96
+ method: "POST",
97
+ body: { languages: [code] },
98
+ });
99
+ }
100
+
101
+ async function addCustomLocale(repo: string, code: string, name: string) {
102
+ const [langPart, regionPart] = code.split("-");
103
+ const url = new URL("/app/settings/multilanguages/custom", await getRepoUrl(repo));
104
+ return await request(url, {
105
+ method: "POST",
106
+ body: {
107
+ lang: { label: name, id: langPart || code },
108
+ region: { label: name, id: regionPart || langPart || code },
109
+ },
110
+ });
111
+ }
112
+
113
+ function handleUnauthenticated() {
114
+ console.error("Not logged in. Run `prismic login` first.");
115
+ process.exitCode = 1;
116
+ }
@@ -0,0 +1,107 @@
1
+ import { parseArgs } from "node:util";
2
+ import * as v from "valibot";
3
+
4
+ import { isAuthenticated } from "./lib/auth";
5
+ import { safeGetRepositoryFromConfig } from "./lib/config";
6
+ import { stringify } from "./lib/json";
7
+ import { ForbiddenRequestError, type ParsedRequestResponse, request } from "./lib/request";
8
+ import { getInternalApiUrl } from "./lib/url";
9
+
10
+ const HELP = `
11
+ List all locales in a Prismic repository.
12
+
13
+ By default, this command reads the repository from prismic.config.json at the
14
+ project root.
15
+
16
+ USAGE
17
+ prismic locale list [flags]
18
+
19
+ FLAGS
20
+ --json Output as JSON
21
+ -r, --repo string Repository domain
22
+ -h, --help Show help for command
23
+
24
+ LEARN MORE
25
+ Use \`prismic <command> <subcommand> --help\` for more information about a command.
26
+ `.trim();
27
+
28
+ export async function localeList(): Promise<void> {
29
+ const {
30
+ values: { help, repo = await safeGetRepositoryFromConfig(), json },
31
+ } = parseArgs({
32
+ args: process.argv.slice(4), // skip: node, script, "locale", "list"
33
+ options: {
34
+ json: { type: "boolean" },
35
+ repo: { type: "string", short: "r" },
36
+ help: { type: "boolean", short: "h" },
37
+ },
38
+ allowPositionals: false,
39
+ });
40
+
41
+ if (help) {
42
+ console.info(HELP);
43
+ return;
44
+ }
45
+
46
+ if (!repo) {
47
+ console.error("Missing prismic.config.json or --repo option");
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+
52
+ const authenticated = await isAuthenticated();
53
+ if (!authenticated) {
54
+ handleUnauthenticated();
55
+ return;
56
+ }
57
+
58
+ const response = await getLocales(repo);
59
+ if (!response.ok) {
60
+ if (response.error instanceof ForbiddenRequestError) {
61
+ handleUnauthenticated();
62
+ } else if (v.isValiError(response.error)) {
63
+ console.error(
64
+ `Failed to list locales: Invalid response: ${stringify(response.error.issues)}`,
65
+ );
66
+ process.exitCode = 1;
67
+ } else {
68
+ console.error(`Failed to list locales: ${stringify(response.value)}`);
69
+ process.exitCode = 1;
70
+ }
71
+ return;
72
+ }
73
+
74
+ const locales = response.value.results;
75
+ if (json) {
76
+ console.info(stringify(locales));
77
+ } else {
78
+ for (const locale of locales) {
79
+ const defaultLabel = locale.isMaster ? " (default)" : "";
80
+ console.info(`${locale.id} ${locale.label}${defaultLabel}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ const LocaleSchema = v.object({
86
+ id: v.string(),
87
+ label: v.string(),
88
+ customName: v.nullable(v.string()),
89
+ isMaster: v.boolean(),
90
+ });
91
+ export type Locale = v.InferOutput<typeof LocaleSchema>;
92
+
93
+ const GetLocalesResponseSchema = v.object({
94
+ results: v.array(LocaleSchema),
95
+ });
96
+ type GetLocalesResponse = v.InferOutput<typeof GetLocalesResponseSchema>;
97
+
98
+ export async function getLocales(repo: string): Promise<ParsedRequestResponse<GetLocalesResponse>> {
99
+ const url = new URL("/locale/repository/locales", await getInternalApiUrl());
100
+ url.searchParams.set("repository", repo);
101
+ return await request(url, { schema: GetLocalesResponseSchema });
102
+ }
103
+
104
+ function handleUnauthenticated() {
105
+ console.error("Not logged in. Run `prismic login` first.");
106
+ process.exitCode = 1;
107
+ }