prismic 0.0.0-pr.28.59bf330

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 (158) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +69 -0
  3. package/dist/builders-hKD4IrLX-DsO7BUQw.mjs +97 -0
  4. package/dist/dist-B11B2hHn.mjs +1 -0
  5. package/dist/dist-DT8CtumB.mjs +1 -0
  6. package/dist/framework-CfjEoVk0.mjs +17 -0
  7. package/dist/index.mjs +2537 -0
  8. package/dist/nextjs-9z7YrSnS.mjs +312 -0
  9. package/dist/nuxt-KoJ61G2q.mjs +59 -0
  10. package/dist/sveltekit-DjXKCG78.mjs +226 -0
  11. package/package.json +58 -0
  12. package/src/codegen-types.ts +82 -0
  13. package/src/codegen.ts +45 -0
  14. package/src/custom-type-add-field-boolean.ts +185 -0
  15. package/src/custom-type-add-field-color.ts +168 -0
  16. package/src/custom-type-add-field-date.ts +171 -0
  17. package/src/custom-type-add-field-embed.ts +168 -0
  18. package/src/custom-type-add-field-geo-point.ts +165 -0
  19. package/src/custom-type-add-field-group.ts +142 -0
  20. package/src/custom-type-add-field-image.ts +168 -0
  21. package/src/custom-type-add-field-key-text.ts +168 -0
  22. package/src/custom-type-add-field-link.ts +191 -0
  23. package/src/custom-type-add-field-number.ts +200 -0
  24. package/src/custom-type-add-field-rich-text.ts +192 -0
  25. package/src/custom-type-add-field-select.ts +174 -0
  26. package/src/custom-type-add-field-timestamp.ts +171 -0
  27. package/src/custom-type-add-field-uid.ts +151 -0
  28. package/src/custom-type-add-field.ts +116 -0
  29. package/src/custom-type-connect-slice.ts +178 -0
  30. package/src/custom-type-create.ts +98 -0
  31. package/src/custom-type-disconnect-slice.ts +134 -0
  32. package/src/custom-type-list.ts +110 -0
  33. package/src/custom-type-remove-field.ts +135 -0
  34. package/src/custom-type-remove.ts +103 -0
  35. package/src/custom-type-set-name.ts +102 -0
  36. package/src/custom-type-view.ts +118 -0
  37. package/src/custom-type.ts +85 -0
  38. package/src/docs-fetch.ts +146 -0
  39. package/src/docs-list.ts +131 -0
  40. package/src/docs.ts +54 -0
  41. package/src/env.d.ts +12 -0
  42. package/src/framework/index.ts +399 -0
  43. package/src/framework/nextjs.templates.ts +426 -0
  44. package/src/framework/nextjs.ts +216 -0
  45. package/src/framework/nuxt.templates.ts +74 -0
  46. package/src/framework/nuxt.ts +250 -0
  47. package/src/framework/sveltekit.templates.ts +278 -0
  48. package/src/framework/sveltekit.ts +241 -0
  49. package/src/index.ts +155 -0
  50. package/src/init.ts +173 -0
  51. package/src/lib/auth.ts +200 -0
  52. package/src/lib/browser.ts +11 -0
  53. package/src/lib/config.ts +111 -0
  54. package/src/lib/custom-types-api.ts +385 -0
  55. package/src/lib/field-path.ts +81 -0
  56. package/src/lib/file.ts +49 -0
  57. package/src/lib/json.ts +3 -0
  58. package/src/lib/packageJson.ts +35 -0
  59. package/src/lib/profile.ts +39 -0
  60. package/src/lib/request.ts +116 -0
  61. package/src/lib/segment.ts +145 -0
  62. package/src/lib/sentry.ts +63 -0
  63. package/src/lib/string.ts +10 -0
  64. package/src/lib/url.ts +31 -0
  65. package/src/locale-add.ts +116 -0
  66. package/src/locale-list.ts +107 -0
  67. package/src/locale-remove.ts +88 -0
  68. package/src/locale-set-default.ts +131 -0
  69. package/src/locale.ts +60 -0
  70. package/src/login.ts +45 -0
  71. package/src/logout.ts +36 -0
  72. package/src/page-type-add-field-boolean.ts +179 -0
  73. package/src/page-type-add-field-color.ts +165 -0
  74. package/src/page-type-add-field-date.ts +168 -0
  75. package/src/page-type-add-field-embed.ts +165 -0
  76. package/src/page-type-add-field-geo-point.ts +162 -0
  77. package/src/page-type-add-field-group.ts +139 -0
  78. package/src/page-type-add-field-image.ts +165 -0
  79. package/src/page-type-add-field-key-text.ts +165 -0
  80. package/src/page-type-add-field-link.ts +188 -0
  81. package/src/page-type-add-field-number.ts +197 -0
  82. package/src/page-type-add-field-rich-text.ts +189 -0
  83. package/src/page-type-add-field-select.ts +171 -0
  84. package/src/page-type-add-field-timestamp.ts +168 -0
  85. package/src/page-type-add-field-uid.ts +148 -0
  86. package/src/page-type-add-field.ts +116 -0
  87. package/src/page-type-connect-slice.ts +178 -0
  88. package/src/page-type-create.ts +128 -0
  89. package/src/page-type-disconnect-slice.ts +134 -0
  90. package/src/page-type-list.ts +109 -0
  91. package/src/page-type-remove-field.ts +135 -0
  92. package/src/page-type-remove.ts +103 -0
  93. package/src/page-type-set-name.ts +102 -0
  94. package/src/page-type-set-repeatable.ts +111 -0
  95. package/src/page-type-view.ts +118 -0
  96. package/src/page-type.ts +90 -0
  97. package/src/preview-add.ts +126 -0
  98. package/src/preview-get-simulator.ts +104 -0
  99. package/src/preview-list.ts +106 -0
  100. package/src/preview-remove-simulator.ts +80 -0
  101. package/src/preview-remove.ts +109 -0
  102. package/src/preview-set-name.ts +137 -0
  103. package/src/preview-set-simulator.ts +116 -0
  104. package/src/preview.ts +75 -0
  105. package/src/pull.ts +236 -0
  106. package/src/push.ts +409 -0
  107. package/src/repo-create.ts +175 -0
  108. package/src/repo-get-access.ts +86 -0
  109. package/src/repo-list.ts +100 -0
  110. package/src/repo-set-access.ts +100 -0
  111. package/src/repo-set-name.ts +102 -0
  112. package/src/repo-view.ts +113 -0
  113. package/src/repo.ts +70 -0
  114. package/src/slice-add-field-boolean.ts +219 -0
  115. package/src/slice-add-field-color.ts +205 -0
  116. package/src/slice-add-field-date.ts +205 -0
  117. package/src/slice-add-field-embed.ts +205 -0
  118. package/src/slice-add-field-geo-point.ts +202 -0
  119. package/src/slice-add-field-group.ts +170 -0
  120. package/src/slice-add-field-image.ts +202 -0
  121. package/src/slice-add-field-key-text.ts +205 -0
  122. package/src/slice-add-field-link.ts +224 -0
  123. package/src/slice-add-field-number.ts +205 -0
  124. package/src/slice-add-field-rich-text.ts +229 -0
  125. package/src/slice-add-field-select.ts +211 -0
  126. package/src/slice-add-field-timestamp.ts +205 -0
  127. package/src/slice-add-field.ts +111 -0
  128. package/src/slice-add-variation.ts +142 -0
  129. package/src/slice-create.ts +164 -0
  130. package/src/slice-list-variations.ts +71 -0
  131. package/src/slice-list.ts +60 -0
  132. package/src/slice-remove-field.ts +125 -0
  133. package/src/slice-remove-variation.ts +113 -0
  134. package/src/slice-remove.ts +92 -0
  135. package/src/slice-rename.ts +104 -0
  136. package/src/slice-set-screenshot.ts +239 -0
  137. package/src/slice-view.ts +83 -0
  138. package/src/slice.ts +95 -0
  139. package/src/status.ts +834 -0
  140. package/src/sync.ts +259 -0
  141. package/src/token-create.ts +203 -0
  142. package/src/token-delete.ts +182 -0
  143. package/src/token-list.ts +223 -0
  144. package/src/token-set-name.ts +193 -0
  145. package/src/token.ts +60 -0
  146. package/src/webhook-add-header.ts +118 -0
  147. package/src/webhook-create.ts +152 -0
  148. package/src/webhook-disable.ts +109 -0
  149. package/src/webhook-enable.ts +132 -0
  150. package/src/webhook-list.ts +93 -0
  151. package/src/webhook-remove-header.ts +117 -0
  152. package/src/webhook-remove.ts +106 -0
  153. package/src/webhook-set-triggers.ts +148 -0
  154. package/src/webhook-status.ts +90 -0
  155. package/src/webhook-test.ts +106 -0
  156. package/src/webhook-view.ts +147 -0
  157. package/src/webhook.ts +95 -0
  158. package/src/whoami.ts +62 -0
@@ -0,0 +1,81 @@
1
+ export type FieldPath =
2
+ | { type: "top-level"; fieldId: string }
3
+ | { type: "nested"; groupId: string; nestedFieldId: string };
4
+
5
+ export function parseFieldPath(fieldId: string): FieldPath {
6
+ const parts = fieldId.split(".");
7
+
8
+ if (parts.length === 1) {
9
+ return { type: "top-level", fieldId };
10
+ }
11
+
12
+ if (parts.length === 2) {
13
+ return { type: "nested", groupId: parts[0], nestedFieldId: parts[1] };
14
+ }
15
+
16
+ // More than 2 parts means nested groups which aren't supported
17
+ return { type: "nested", groupId: parts[0], nestedFieldId: parts.slice(1).join(".") };
18
+ }
19
+
20
+ export type GroupFieldResult =
21
+ | { ok: true; group: { config: { fields: Record<string, unknown> } } }
22
+ | { ok: false; error: string };
23
+
24
+ export function isGroupField(field: unknown): field is { type: "Group"; config: { fields: Record<string, unknown> } } {
25
+ return (
26
+ typeof field === "object" &&
27
+ field !== null &&
28
+ "type" in field &&
29
+ field.type === "Group" &&
30
+ "config" in field &&
31
+ typeof field.config === "object" &&
32
+ field.config !== null &&
33
+ "fields" in field.config
34
+ );
35
+ }
36
+
37
+ export function findGroupInTab(
38
+ tabFields: Record<string, unknown>,
39
+ groupId: string,
40
+ tabName: string,
41
+ ): GroupFieldResult {
42
+ const field = tabFields[groupId];
43
+
44
+ if (!field) {
45
+ return { ok: false, error: `Group "${groupId}" not found in tab "${tabName}"` };
46
+ }
47
+
48
+ if (!isGroupField(field)) {
49
+ return { ok: false, error: `Field "${groupId}" is not a group` };
50
+ }
51
+
52
+ return { ok: true, group: field };
53
+ }
54
+
55
+ export function findGroupInVariation(
56
+ primary: Record<string, unknown>,
57
+ groupId: string,
58
+ variationId: string,
59
+ ): GroupFieldResult {
60
+ const field = primary[groupId];
61
+
62
+ if (!field) {
63
+ return { ok: false, error: `Group "${groupId}" not found in variation "${variationId}"` };
64
+ }
65
+
66
+ if (!isGroupField(field)) {
67
+ return { ok: false, error: `Field "${groupId}" is not a group` };
68
+ }
69
+
70
+ return { ok: true, group: field };
71
+ }
72
+
73
+ export function validateNestedFieldPath(fieldPath: FieldPath): { ok: true } | { ok: false; error: string } {
74
+ if (fieldPath.type === "nested" && fieldPath.nestedFieldId.includes(".")) {
75
+ return {
76
+ ok: false,
77
+ error: `Nested groups not supported. Use: group.field`,
78
+ };
79
+ }
80
+ return { ok: true };
81
+ }
@@ -0,0 +1,49 @@
1
+ import { access } from "node:fs/promises";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { appendTrailingSlash } from "./url";
5
+
6
+ export async function findUpward(
7
+ name: string,
8
+ config: { start?: URL; stop?: URL | string } = {},
9
+ ): Promise<URL | undefined> {
10
+ const { start = pathToFileURL(process.cwd()), stop } = config;
11
+
12
+ let dir = appendTrailingSlash(start);
13
+
14
+ while (true) {
15
+ const path = new URL(name, dir);
16
+ try {
17
+ await access(path);
18
+ return path;
19
+ } catch {}
20
+
21
+ if (typeof stop === "string") {
22
+ const stopPath = new URL(stop, dir);
23
+ try {
24
+ await access(stopPath);
25
+ return;
26
+ } catch {}
27
+ } else if (stop instanceof URL) {
28
+ if (stop.href === dir.href) {
29
+ return;
30
+ }
31
+ }
32
+
33
+ const parent = new URL("..", dir);
34
+ if (parent.href === dir.href) {
35
+ return undefined;
36
+ }
37
+
38
+ dir = parent;
39
+ }
40
+ }
41
+
42
+ export async function exists(path: URL): Promise<boolean> {
43
+ try {
44
+ await access(path);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ export function stringify(input: unknown): string {
2
+ return JSON.stringify(input, null, 2);
3
+ }
@@ -0,0 +1,35 @@
1
+ import detectIndent from "detect-indent";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+
4
+ import { findUpward } from "./file";
5
+
6
+ export async function addDependencies(
7
+ dependencies: Record<string, string>,
8
+ ): Promise<void> {
9
+ const packageJsonPath = await findUpward("package.json");
10
+ if (!packageJsonPath) {
11
+ throw new Error("No package.json found");
12
+ }
13
+ const raw = await readFile(packageJsonPath, "utf8");
14
+ const indent = detectIndent(raw).indent || "\t";
15
+ const packageJson = JSON.parse(raw);
16
+ packageJson.dependencies = Object.fromEntries(
17
+ Object.entries({ ...packageJson.dependencies, ...dependencies }).sort(
18
+ ([a], [b]) => a.localeCompare(b),
19
+ ),
20
+ );
21
+ await writeFile(
22
+ packageJsonPath,
23
+ JSON.stringify(packageJson, null, indent) + "\n",
24
+ );
25
+ }
26
+
27
+ export async function getNpmPackageVersion(
28
+ name: string,
29
+ tag = "latest",
30
+ ): Promise<string> {
31
+ const url = new URL(`${name}/${tag}`, "https://registry.npmjs.org/");
32
+ const res = await fetch(url);
33
+ const { version } = await res.json();
34
+ return version;
35
+ }
@@ -0,0 +1,39 @@
1
+ import * as v from "valibot";
2
+
3
+ import { readToken } from "./auth";
4
+ import { getUserServiceUrl } from "./url";
5
+
6
+ const PrismicUserProfileSchema = v.object({
7
+ userId: v.string(),
8
+ shortId: v.string(),
9
+ intercomHash: v.string(),
10
+ email: v.string(),
11
+ firstName: v.string(),
12
+ lastName: v.string(),
13
+ });
14
+
15
+ export type PrismicUserProfile = v.InferOutput<typeof PrismicUserProfileSchema>;
16
+
17
+ export async function getProfile(): Promise<PrismicUserProfile> {
18
+ const token = await readToken();
19
+ if (!token) {
20
+ throw new Error("Not authenticated. Log in before trying again.");
21
+ }
22
+
23
+ const baseUrl = await getUserServiceUrl();
24
+ const url = new URL("profile", baseUrl);
25
+
26
+ const response = await fetch(url, {
27
+ headers: {
28
+ Authorization: `Bearer ${token}`,
29
+ },
30
+ });
31
+
32
+ if (!response.ok) {
33
+ throw new Error("Failed to retrieve profile from the Prismic user service.");
34
+ }
35
+
36
+ const json = await response.json();
37
+
38
+ return v.parse(PrismicUserProfileSchema, json);
39
+ }
@@ -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,145 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ import packageJson from "../../package.json" with { type: "json" };
8
+
9
+ const SEGMENT_WRITE_KEY =
10
+ process.env.PRISMIC_ENV && process.env.PRISMIC_ENV !== "production"
11
+ ? "Ng5oKJHCGpSWplZ9ymB7Pu7rm0sTDeiG"
12
+ : "cGjidifKefYb6EPaGaqpt8rQXkv5TD6P";
13
+ const SEGMENT_TRACK_URL = "https://api.segment.io/v1/track";
14
+
15
+ let enabled = false;
16
+ let anonymousId = "";
17
+ let authorization = "";
18
+ const appContext = { app: { name: packageJson.name, version: packageJson.version } };
19
+ const eventQueue: Array<{ event: string; properties: Record<string, unknown> }> = [];
20
+
21
+ export async function initSegment(): Promise<void> {
22
+ try {
23
+ enabled = await isTelemetryEnabled();
24
+ if (!enabled) {
25
+ return;
26
+ }
27
+
28
+ anonymousId = randomUUID();
29
+ authorization = `Basic ${btoa(SEGMENT_WRITE_KEY + ":")}`;
30
+ process.on("exit", flushTelemetry);
31
+ } catch {
32
+ enabled = false;
33
+ }
34
+ }
35
+
36
+ export function trackStart(command: string): void {
37
+ if (!enabled) {
38
+ return;
39
+ }
40
+
41
+ eventQueue.push({
42
+ event: "Prismic CLI Start",
43
+ properties: {
44
+ commandType: command,
45
+ fullCommand: process.argv.join(" "),
46
+ },
47
+ });
48
+ }
49
+
50
+ export function trackEnd(
51
+ command: string,
52
+ success: boolean,
53
+ error?: unknown,
54
+ ): void {
55
+ if (!enabled) {
56
+ return;
57
+ }
58
+
59
+ const properties: Record<string, unknown> = {
60
+ commandType: command,
61
+ fullCommand: process.argv.join(" "),
62
+ success,
63
+ };
64
+
65
+ if (error !== undefined) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ properties.error = message.slice(0, 512);
68
+ }
69
+
70
+ eventQueue.push({ event: "Prismic CLI End", properties });
71
+ }
72
+
73
+ /**
74
+ * Spawns a detached subprocess to send queued telemetry events.
75
+ * The main process exits immediately; the subprocess handles HTTP delivery.
76
+ */
77
+ function flushTelemetry(): void {
78
+ if (eventQueue.length === 0) {
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const payload = Buffer.from(
84
+ JSON.stringify({
85
+ url: SEGMENT_TRACK_URL,
86
+ authorization,
87
+ events: eventQueue.map((e) => ({
88
+ anonymousId,
89
+ event: e.event,
90
+ properties: { nodeVersion: process.versions.node, ...e.properties },
91
+ context: appContext,
92
+ timestamp: new Date().toISOString(),
93
+ })),
94
+ }),
95
+ ).toString("base64");
96
+
97
+ const child = spawn(
98
+ process.execPath,
99
+ ["--input-type=module", "-e", FLUSH_SCRIPT, payload],
100
+ { detached: true, stdio: "ignore" },
101
+ );
102
+ child.unref();
103
+ } catch {
104
+ // Silent failure — never breaks the CLI
105
+ }
106
+
107
+ eventQueue.length = 0;
108
+ }
109
+
110
+ const FLUSH_SCRIPT = `
111
+ const {url, authorization, events} = JSON.parse(Buffer.from(process.argv[1], "base64"));
112
+ const h = {"Content-Type": "application/json", Authorization: authorization};
113
+ await Promise.allSettled(events.map(e => fetch(url, {method: "POST", headers: h, body: JSON.stringify(e)}).catch(() => {})));
114
+ `;
115
+
116
+ async function isTelemetryEnabled(): Promise<boolean> {
117
+ try {
118
+ // Check user-level .prismicrc
119
+ const userRc = await readJsonFile(join(homedir(), ".prismicrc"));
120
+ if (userRc?.telemetry === false) {
121
+ return false;
122
+ }
123
+
124
+ // Check project-level .prismicrc
125
+ const projectRc = await readJsonFile(join(process.cwd(), ".prismicrc"));
126
+ if (projectRc?.telemetry === false) {
127
+ return false;
128
+ }
129
+
130
+ return true;
131
+ } catch {
132
+ return true;
133
+ }
134
+ }
135
+
136
+ async function readJsonFile(
137
+ path: string,
138
+ ): Promise<Record<string, unknown> | undefined> {
139
+ try {
140
+ const contents = await readFile(path, "utf8");
141
+ return JSON.parse(contents);
142
+ } catch {
143
+ return undefined;
144
+ }
145
+ }
@@ -0,0 +1,63 @@
1
+ import * as Sentry from "@sentry/node-core/light";
2
+
3
+ import packageJson from "../../package.json" with { type: "json" };
4
+
5
+ const SENTRY_DSN =
6
+ import.meta.env.PRISMIC_SENTRY_DSN ||
7
+ "https://e1886b1775bd397cd1afc60bfd2ebfc8@o146123.ingest.us.sentry.io/4510445143588864";
8
+
9
+ function isSentryEnabled(): boolean {
10
+ if (import.meta.env.PRISMIC_SENTRY_ENABLED === undefined) {
11
+ return import.meta.env.PROD;
12
+ }
13
+
14
+ return import.meta.env.PRISMIC_SENTRY_ENABLED === "true";
15
+ }
16
+
17
+ export function setupSentry(): void {
18
+ try {
19
+ if (!isSentryEnabled()) {
20
+ return;
21
+ }
22
+
23
+ Sentry.init({
24
+ dsn: SENTRY_DSN,
25
+ release: packageJson.version,
26
+ environment: import.meta.env.PRISMIC_SENTRY_ENVIRONMENT ?? "production",
27
+ defaultIntegrations: false,
28
+ integrations: [],
29
+ maxValueLength: 2_500,
30
+ });
31
+
32
+ Sentry.setContext("Process", {
33
+ command: process.argv.join(" "),
34
+ cwd: process.cwd(),
35
+ });
36
+ } catch {
37
+ // Silent failure — never breaks the CLI
38
+ }
39
+ }
40
+
41
+ export async function captureError(error: unknown): Promise<void> {
42
+ try {
43
+ if (!isSentryEnabled()) {
44
+ return;
45
+ }
46
+
47
+ Sentry.captureException(
48
+ error,
49
+ error instanceof Error
50
+ ? { extra: { cause: error.cause, fullCommand: process.argv.join(" ") } }
51
+ : {},
52
+ );
53
+
54
+ await Sentry.flush();
55
+ } catch {
56
+ // Silent failure — never breaks the CLI
57
+ }
58
+ }
59
+
60
+ // Re-exports for future devtools-parity integration points
61
+ export const setUser = Sentry.setUser;
62
+ export const setTag = Sentry.setTag;
63
+ export const setContext = Sentry.setContext;
@@ -0,0 +1,10 @@
1
+ import baseDedent from "dedent";
2
+
3
+ export function humanReadable(id: string): string {
4
+ return id
5
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
6
+ .replace(/[_-]+/g, " ")
7
+ .replace(/\b\w/g, (c) => c.toUpperCase());
8
+ }
9
+
10
+ export const dedent = baseDedent.withOptions({ alignValues: true });
package/src/lib/url.ts ADDED
@@ -0,0 +1,31 @@
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 async function getAuthUrl(): Promise<URL> {
22
+ const host = await readHost();
23
+ host.hostname = `auth.${host.hostname}`;
24
+ return appendTrailingSlash(host);
25
+ }
26
+
27
+ export function appendTrailingSlash(url: string | URL): URL {
28
+ const newURL = new URL(url);
29
+ if (!newURL.pathname.endsWith("/")) newURL.pathname += "/";
30
+ return newURL;
31
+ }
@@ -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
+ }