prismic 0.0.0-canary.2cfb4a8

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.
@@ -0,0 +1,241 @@
1
+ import type { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
2
+
3
+ import { loadFile } from "magicast";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+
7
+ import type { Framework } from ".";
8
+
9
+ import { FrameworkAdapter } from ".";
10
+ import { exists } from "../lib/file";
11
+ import { getNpmPackageVersion } from "../lib/packageJson";
12
+ import { dedent } from "../lib/string";
13
+ import {
14
+ previewAPIRouteTemplate,
15
+ prismicIOFileTemplate,
16
+ rootLayoutTemplate,
17
+ sliceSimulatorPageTemplate,
18
+ sliceTemplate,
19
+ } from "./sveltekit.templates";
20
+
21
+ export class SvelteKitFramework extends FrameworkAdapter {
22
+ readonly id: Framework = "sveltekit";
23
+
24
+ async getDependencies(): Promise<Record<string, string>> {
25
+ return {
26
+ "@prismicio/client": `^${await getNpmPackageVersion("@prismicio/client")}`,
27
+ "@prismicio/svelte": `^${await getNpmPackageVersion("@prismicio/svelte")}`,
28
+ };
29
+ }
30
+
31
+ async initProject(): Promise<void> {
32
+ await super.initProject();
33
+
34
+ await this.#createPrismicIOFile();
35
+ await this.#createSliceSimulatorPage();
36
+ await this.#createPreviewRouteMatcher();
37
+ await this.#createPreviewAPIRoute();
38
+ await this.#createPreviewRouteDirectory();
39
+ await this.#createRootLayoutServerFile();
40
+ await this.#createRootLayoutFile();
41
+ await this.#modifyViteConfig();
42
+ }
43
+
44
+ async createSliceComponent(
45
+ model: SharedSlice,
46
+ sliceDirectory: URL,
47
+ ): Promise<{ componentPath: URL }> {
48
+ const componentPath = new URL("index.svelte", sliceDirectory);
49
+ const contents = sliceTemplate({
50
+ name: model.name,
51
+ typescript: await this.checkIsTypeScriptProject(),
52
+ version: await this.#getSvelteMajor(),
53
+ });
54
+ await writeFile(componentPath, contents);
55
+ return { componentPath };
56
+ }
57
+
58
+ getSliceImportPath(relativeDirectory: string): string {
59
+ return `./${relativeDirectory}/index.svelte`;
60
+ }
61
+
62
+ async getDefaultSliceLibraryPath(projectRoot: URL): Promise<URL> {
63
+ return new URL("src/lib/slices/", projectRoot);
64
+ }
65
+
66
+ async getClientFilePath(): Promise<string | null> {
67
+ return "src/lib/prismicio.ts";
68
+ }
69
+
70
+ async getSlicesDirectoryPath(): Promise<string> {
71
+ return "src/lib/slices/";
72
+ }
73
+
74
+ getSliceComponentExtensions(): string[] {
75
+ return [".svelte"];
76
+ }
77
+
78
+ async getRoutePath(route: string): Promise<{ path: string; extensions: string[] } | null> {
79
+ switch (route) {
80
+ case "/slice-simulator":
81
+ return { path: "src/routes/slice-simulator/+page", extensions: [".svelte"] };
82
+ case "/api/preview":
83
+ return { path: "src/routes/api/preview/+server", extensions: [".ts", ".js"] };
84
+ default:
85
+ return null;
86
+ }
87
+ }
88
+
89
+ async #createPrismicIOFile(): Promise<void> {
90
+ const extension = await this.getJsFileExtension();
91
+ const projectRoot = await this.getProjectRoot();
92
+ const filePath = new URL(`src/lib/prismicio.${extension}`, projectRoot);
93
+
94
+ if (await exists(filePath)) {
95
+ return;
96
+ }
97
+
98
+ const typescript = await this.checkIsTypeScriptProject();
99
+ const contents = prismicIOFileTemplate({ typescript });
100
+ await mkdir(new URL(".", filePath), { recursive: true });
101
+ await writeFile(filePath, contents);
102
+ }
103
+
104
+ async #createSliceSimulatorPage(): Promise<void> {
105
+ const projectRoot = await this.getProjectRoot();
106
+ const filePath = new URL("src/routes/slice-simulator/+page.svelte", projectRoot);
107
+
108
+ if (await exists(filePath)) {
109
+ return;
110
+ }
111
+
112
+ const contents = sliceSimulatorPageTemplate({
113
+ version: await this.#getSvelteMajor(),
114
+ });
115
+ await mkdir(new URL(".", filePath), { recursive: true });
116
+ await writeFile(filePath, contents);
117
+ }
118
+
119
+ async #createPreviewRouteMatcher(): Promise<void> {
120
+ const extension = await this.getJsFileExtension();
121
+ const projectRoot = await this.getProjectRoot();
122
+ const filePath = new URL(`src/params/preview.${extension}`, projectRoot);
123
+
124
+ if (await exists(filePath)) {
125
+ return;
126
+ }
127
+
128
+ const contents = dedent`
129
+ export function match(param) {
130
+ return param === 'preview';
131
+ }
132
+ `;
133
+ await mkdir(new URL(".", filePath), { recursive: true });
134
+ await writeFile(filePath, contents);
135
+ }
136
+
137
+ async #createPreviewAPIRoute(): Promise<void> {
138
+ const extension = await this.getJsFileExtension();
139
+ const projectRoot = await this.getProjectRoot();
140
+ const filePath = new URL(`src/routes/api/preview/+server.${extension}`, projectRoot);
141
+
142
+ if (await exists(filePath)) {
143
+ return;
144
+ }
145
+
146
+ const typescript = await this.checkIsTypeScriptProject();
147
+ const contents = previewAPIRouteTemplate({ typescript });
148
+ await mkdir(new URL(".", filePath), { recursive: true });
149
+ await writeFile(filePath, contents);
150
+ }
151
+
152
+ async #createPreviewRouteDirectory(): Promise<void> {
153
+ const projectRoot = await this.getProjectRoot();
154
+ const filePath = new URL("src/routes/[[preview=preview]]/README.md", projectRoot);
155
+
156
+ if (await exists(filePath)) {
157
+ return;
158
+ }
159
+
160
+ const contents = dedent`
161
+ This directory adds support for optional \`/preview\` routes. Do not remove this directory.
162
+
163
+ All routes within this directory will be served using the following URLs:
164
+
165
+ - \`/example-route\` (prerendered)
166
+ - \`/preview/example-route\` (server-rendered)
167
+
168
+ See <https://prismic.io/docs/svelte-preview> for more information.
169
+ `;
170
+ await mkdir(new URL(".", filePath), { recursive: true });
171
+ await writeFile(filePath, contents);
172
+ }
173
+
174
+ async #createRootLayoutServerFile(): Promise<void> {
175
+ const extension = await this.getJsFileExtension();
176
+ const projectRoot = await this.getProjectRoot();
177
+ const filePath = new URL(`src/routes/+layout.server.${extension}`, projectRoot);
178
+
179
+ if (await exists(filePath)) {
180
+ return;
181
+ }
182
+
183
+ const contents = dedent`
184
+ export const prerender = "auto";
185
+ `;
186
+ await mkdir(new URL(".", filePath), { recursive: true });
187
+ await writeFile(filePath, contents);
188
+ }
189
+
190
+ async #createRootLayoutFile(): Promise<void> {
191
+ const projectRoot = await this.getProjectRoot();
192
+ const filePath = new URL("src/routes/+layout.svelte", projectRoot);
193
+
194
+ if (await exists(filePath)) {
195
+ return;
196
+ }
197
+
198
+ const contents = rootLayoutTemplate({
199
+ version: await this.#getSvelteMajor(),
200
+ });
201
+ await mkdir(new URL(".", filePath), { recursive: true });
202
+ await writeFile(filePath, contents);
203
+ }
204
+
205
+ async #modifyViteConfig(): Promise<void> {
206
+ const projectRoot = await this.getProjectRoot();
207
+ let configUrl = new URL("vite.config.js", projectRoot);
208
+ if (!(await exists(configUrl))) {
209
+ configUrl = new URL("vite.config.ts", projectRoot);
210
+ }
211
+ if (!(await exists(configUrl))) {
212
+ return;
213
+ }
214
+
215
+ const filepath = configUrl.pathname;
216
+ const mod = await loadFile(filepath);
217
+ if (mod.exports.default.$type !== "function-call") {
218
+ return;
219
+ }
220
+
221
+ const config = mod.exports.default.$args[0];
222
+ config.server ??= {};
223
+ config.server.fs ??= {};
224
+ config.server.fs.allow ??= [];
225
+ if (!config.server.fs.allow.includes("./prismic.config.json")) {
226
+ config.server.fs.allow.push("./prismic.config.json");
227
+ }
228
+
229
+ const contents = mod.generate().code.replace(/\n\s*\n(?=\s*server:)/, "\n");
230
+ await writeFile(configUrl, contents);
231
+ }
232
+
233
+ async #getSvelteMajor(): Promise<number> {
234
+ const projectRoot = await this.getProjectRoot();
235
+ const require = createRequire(new URL("package.json", projectRoot));
236
+ const { version } = require("svelte/package.json");
237
+ const major = Number.parseInt(version.split(".")[0]);
238
+ if (Number.isNaN(major)) return Infinity;
239
+ return major;
240
+ }
241
+ }
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from "node:util";
4
+
5
+ import packageJson from "../package.json" with { type: "json" };
6
+ import { init } from "./init";
7
+ import { initSegment, trackEnd, trackStart } from "./lib/segment";
8
+ import { captureError, setupSentry } from "./lib/sentry";
9
+ import { login } from "./login";
10
+ import { logout } from "./logout";
11
+ import { sync } from "./sync";
12
+ import { whoami } from "./whoami";
13
+
14
+ const HELP = `
15
+ Prismic CLI for managing repositories and configurations.
16
+
17
+ USAGE
18
+ prismic <command> [flags]
19
+
20
+ COMMANDS
21
+ init Initialize a Prismic project
22
+ sync Sync types and slices from Prismic
23
+ login Log in to Prismic
24
+ logout Log out of Prismic
25
+ whoami Show the currently logged in user
26
+
27
+ FLAGS
28
+ -v, --version Show CLI version
29
+ -h, --help Show help for command
30
+
31
+ LEARN MORE
32
+ Use \`prismic <command> --help\` for more information about a command.
33
+ `.trim();
34
+
35
+ const {
36
+ positionals,
37
+ values: { version },
38
+ } = parseArgs({
39
+ options: {
40
+ help: { type: "boolean", short: "h" },
41
+ version: { type: "boolean", short: "v" },
42
+ },
43
+ allowPositionals: true,
44
+ strict: false,
45
+ });
46
+
47
+ setupSentry();
48
+ await initSegment();
49
+
50
+ if (version) {
51
+ console.info(packageJson.version);
52
+ } else {
53
+ const command = positionals[0];
54
+
55
+ trackStart(command);
56
+
57
+ try {
58
+ switch (command) {
59
+ case "init":
60
+ await init();
61
+ break;
62
+ case "sync":
63
+ await sync();
64
+ break;
65
+ case "login":
66
+ await login();
67
+ break;
68
+ case "logout":
69
+ await logout();
70
+ break;
71
+ case "whoami":
72
+ await whoami();
73
+ break;
74
+ default: {
75
+ if (command) {
76
+ console.error(`Unknown command: ${command}`);
77
+ process.exitCode = 1;
78
+ }
79
+ console.info(HELP);
80
+ }
81
+ }
82
+
83
+ trackEnd(command, process.exitCode !== 1);
84
+ } catch (error) {
85
+ await captureError(error);
86
+ trackEnd(command, false, error);
87
+ process.exitCode = 1;
88
+ throw error;
89
+ }
90
+ }
package/src/init.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { readFile, rm } from "node:fs/promises";
2
+ import { parseArgs } from "node:util";
3
+ import * as v from "valibot";
4
+
5
+ import { createLoginSession, isAuthenticated } from "./lib/auth";
6
+ import { openBrowser } from "./lib/browser";
7
+ import { createConfig, readConfig, UnknownProjectRoot } from "./lib/config";
8
+ import { findUpward } from "./lib/file";
9
+ import { getFramework } from "./framework";
10
+ import { request } from "./lib/request";
11
+ import { getUserServiceUrl } from "./lib/url";
12
+ import { syncCustomTypes, syncSlices } from "./sync";
13
+
14
+ const HELP = `
15
+ Initialize a Prismic project by creating a prismic.config.json file.
16
+
17
+ Detects the project framework, installs dependencies, and syncs models
18
+ from Prismic. If a slicemachine.config.json exists, it will be migrated.
19
+
20
+ USAGE
21
+ prismic init [flags]
22
+
23
+ FLAGS
24
+ -r, --repo string Repository name
25
+ -h, --help Show help for command
26
+
27
+ EXAMPLES
28
+ prismic init --repo my-repo
29
+
30
+ LEARN MORE
31
+ Use \`prismic <command> --help\` for more information about a command.
32
+ `.trim();
33
+
34
+ const ProfileSchema = v.object({
35
+ repositories: v.array(
36
+ v.object({
37
+ domain: v.string(),
38
+ name: v.optional(v.string()),
39
+ }),
40
+ ),
41
+ });
42
+
43
+ export async function init(): Promise<void> {
44
+ const { values } = parseArgs({
45
+ args: process.argv.slice(3),
46
+ options: {
47
+ help: { type: "boolean", short: "h" },
48
+ repo: { type: "string", short: "r" },
49
+ },
50
+ });
51
+
52
+ if (values.help) {
53
+ console.info(HELP);
54
+ return;
55
+ }
56
+
57
+ // Check for existing prismic.config.json
58
+ const existingConfig = await readConfig();
59
+ if (existingConfig.ok) {
60
+ console.error("A prismic.config.json file already exists.");
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+
65
+ // Check for legacy slicemachine.config.json
66
+ const legacyConfigPath = await findUpward("slicemachine.config.json", {
67
+ stop: "package.json",
68
+ });
69
+ let legacyRepoName: string | undefined;
70
+ let legacyLibraries: string[] | undefined;
71
+
72
+ if (legacyConfigPath) {
73
+ try {
74
+ const contents = await readFile(legacyConfigPath, "utf8");
75
+ const legacyConfig = JSON.parse(contents);
76
+ legacyRepoName = legacyConfig.repositoryName;
77
+ legacyLibraries = legacyConfig.libraries;
78
+ } catch {
79
+ console.warn("Could not read slicemachine.config.json, ignoring.");
80
+ }
81
+ }
82
+
83
+ // Determine repo name: --repo flag > legacy config > error
84
+ const repo = values.repo ?? legacyRepoName;
85
+ if (!repo) {
86
+ console.error("Missing required flag: --repo");
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+
91
+ // Login if needed
92
+ if (!(await isAuthenticated())) {
93
+ console.info("Not logged in. Starting login...");
94
+ const { email } = await createLoginSession({
95
+ onReady: (url) => {
96
+ console.info("Opening browser to complete login...");
97
+ console.info(`If the browser doesn't open, visit: ${url}`);
98
+ openBrowser(url);
99
+ },
100
+ });
101
+ console.info(`Logged in as ${email}`);
102
+ }
103
+
104
+ // Validate repo membership
105
+ const profileUrl = new URL("profile", await getUserServiceUrl());
106
+ const profileResponse = await request(profileUrl, {
107
+ schema: ProfileSchema,
108
+ });
109
+ if (!profileResponse.ok) {
110
+ console.error("Failed to fetch user profile.");
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+
115
+ const repoData = profileResponse.value.repositories.find(
116
+ (r) => r.domain === repo,
117
+ );
118
+ if (!repoData) {
119
+ console.error(
120
+ `Repository "${repo}" not found in your account. Check the name or create it with \`prismic repo create\`.`,
121
+ );
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+
126
+ // Detect framework
127
+ const framework = await getFramework();
128
+ if (!framework) {
129
+ console.error(
130
+ "Could not detect a supported framework (Next.js, Nuxt, or SvelteKit).",
131
+ );
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+
136
+ // Create prismic.config.json
137
+ const configData: { repositoryName: string; libraries?: string[] } = {
138
+ repositoryName: repo,
139
+ };
140
+ if (legacyLibraries?.length) {
141
+ configData.libraries = legacyLibraries;
142
+ }
143
+
144
+ const configResult = await createConfig(configData);
145
+ if (!configResult.ok) {
146
+ if (configResult.error instanceof UnknownProjectRoot) {
147
+ console.error(
148
+ "Could not find a package.json file. Run this command from a project directory.",
149
+ );
150
+ } else {
151
+ console.error("Failed to create config file.");
152
+ }
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+
157
+ // Delete legacy config after new config is created
158
+ if (legacyConfigPath) {
159
+ await rm(legacyConfigPath);
160
+ console.info("Migrated slicemachine.config.json to prismic.config.json");
161
+ }
162
+
163
+ // Install dependencies and create framework files
164
+ await framework.initProject();
165
+
166
+ // Sync models from remote
167
+ await syncSlices(repo, framework);
168
+ await syncCustomTypes(repo, framework);
169
+
170
+ console.info(
171
+ `Initialized Prismic for repository "${repo}". Run \`npm install\` to install new dependencies.`,
172
+ );
173
+ }
@@ -0,0 +1,200 @@
1
+ import { access, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { createServer } from "node:http";
3
+ import { homedir } from "node:os";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { appendTrailingSlash } from "./url";
7
+
8
+ const AUTH_FILE_PATH = new URL(
9
+ ".prismic",
10
+ appendTrailingSlash(pathToFileURL(homedir())),
11
+ );
12
+ const DEFAULT_HOST = "https://prismic.io";
13
+ const LOGIN_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
14
+ const PREFERRED_PORT = 5555;
15
+
16
+ type AuthContents = {
17
+ token?: string;
18
+ base?: string;
19
+ };
20
+
21
+ export async function saveToken(
22
+ token: string,
23
+ options?: { base?: string },
24
+ ): Promise<void> {
25
+ const contents: AuthContents = { token, base: options?.base };
26
+ await writeFile(AUTH_FILE_PATH, JSON.stringify(contents, null, 2));
27
+ }
28
+
29
+ export async function isAuthenticated(): Promise<boolean> {
30
+ const token = await readToken();
31
+ if (!token) return false;
32
+
33
+ // Verify token is still valid by calling the profile endpoint
34
+ try {
35
+ const host = await readHost();
36
+ host.hostname = `user-service.${host.hostname}`;
37
+ const url = new URL("profile", host);
38
+
39
+ const response = await fetch(url, {
40
+ headers: {
41
+ Accept: "application/json",
42
+ Cookie: `SESSION=fake_session; prismic-auth=${token}`,
43
+ },
44
+ });
45
+
46
+ if (!response.ok) {
47
+ await removeToken();
48
+ return false;
49
+ }
50
+
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ export async function readToken(): Promise<string | undefined> {
58
+ const auth = await readAuthFile();
59
+ return auth?.token;
60
+ }
61
+
62
+ export async function readHost(): Promise<URL> {
63
+ try {
64
+ const auth = await readAuthFile();
65
+ if (!auth?.base) return new URL(DEFAULT_HOST);
66
+ return new URL(auth.base);
67
+ } catch {
68
+ return new URL(DEFAULT_HOST);
69
+ }
70
+ }
71
+
72
+ export async function createLoginSession(options?: {
73
+ onReady?: (url: URL) => void;
74
+ }): Promise<{ email: string }> {
75
+ const host = await readHost();
76
+ const corsOrigin = host.origin;
77
+
78
+ return new Promise((resolve, reject) => {
79
+ const server = createServer((req, res) => {
80
+ if (req.method === "OPTIONS") {
81
+ res.writeHead(204, {
82
+ "Access-Control-Allow-Origin": corsOrigin,
83
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
84
+ "Access-Control-Allow-Headers": "Content-Type",
85
+ });
86
+ res.end();
87
+ return;
88
+ }
89
+
90
+ if (req.method === "POST") {
91
+ let body = "";
92
+
93
+ req.on("data", (chunk) => {
94
+ body += chunk.toString();
95
+ });
96
+
97
+ req.on("end", async () => {
98
+ try {
99
+ const { cookies, email } = JSON.parse(body);
100
+
101
+ const cookie: string | undefined = cookies.find((c: string) =>
102
+ c.startsWith("prismic-auth="),
103
+ );
104
+ const token = cookie?.split(";")[0]?.replace(/^prismic-auth=/, "");
105
+
106
+ if (!token) {
107
+ res.writeHead(400, {
108
+ "Access-Control-Allow-Origin": corsOrigin,
109
+ "Content-Type": "application/json",
110
+ });
111
+ res.end(JSON.stringify({ error: "Invalid request" }));
112
+ return;
113
+ }
114
+
115
+ await saveToken(token);
116
+
117
+ res.writeHead(200, {
118
+ "Access-Control-Allow-Origin": corsOrigin,
119
+ "Content-Type": "application/json",
120
+ });
121
+ res.end(JSON.stringify({ success: true }));
122
+
123
+ clearTimeout(timeoutId);
124
+ server.close();
125
+ resolve({ email });
126
+ } catch {
127
+ res.writeHead(400, {
128
+ "Access-Control-Allow-Origin": corsOrigin,
129
+ "Content-Type": "application/json",
130
+ });
131
+ res.end(JSON.stringify({ error: "Invalid request" }));
132
+ }
133
+ });
134
+
135
+ return;
136
+ }
137
+
138
+ res.writeHead(404);
139
+ res.end();
140
+ });
141
+
142
+ const timeoutId = setTimeout(() => {
143
+ server.close();
144
+ reject(new Error("Login timed out. Please try again."));
145
+ }, LOGIN_TIMEOUT_MS);
146
+
147
+ const onListening = (): void => {
148
+ const address = server.address();
149
+ if (!address || typeof address === "string") {
150
+ clearTimeout(timeoutId);
151
+ server.close();
152
+ reject(new Error("Failed to start login server"));
153
+ return;
154
+ }
155
+
156
+ const url = buildLoginUrl(host, address.port);
157
+ options?.onReady?.(url);
158
+ };
159
+
160
+ server.on("error", (error: NodeJS.ErrnoException) => {
161
+ if (error.code === "EADDRINUSE" && server.listening === false) {
162
+ server.listen(0, "0.0.0.0", onListening);
163
+ } else {
164
+ clearTimeout(timeoutId);
165
+ reject(error);
166
+ }
167
+ });
168
+
169
+ server.listen(PREFERRED_PORT, "0.0.0.0", onListening);
170
+ });
171
+ }
172
+
173
+ function buildLoginUrl(host: URL, port: number): URL {
174
+ const url = new URL("/dashboard/cli/login", host);
175
+ url.searchParams.set("source", "prismic-cli");
176
+ url.searchParams.set("port", port.toString());
177
+ return url;
178
+ }
179
+
180
+ async function readAuthFile(): Promise<AuthContents | undefined> {
181
+ try {
182
+ const contents = await readFile(AUTH_FILE_PATH, "utf-8");
183
+ return JSON.parse(contents);
184
+ } catch {
185
+ return undefined;
186
+ }
187
+ }
188
+
189
+ export async function removeToken(): Promise<boolean> {
190
+ try {
191
+ await access(AUTH_FILE_PATH);
192
+ } catch {
193
+ return true;
194
+ }
195
+
196
+ const auth = await readAuthFile();
197
+ if (!auth) return false;
198
+ await rm(AUTH_FILE_PATH);
199
+ return true;
200
+ }
@@ -0,0 +1,11 @@
1
+ import { exec } from "node:child_process";
2
+
3
+ export function openBrowser(url: URL): void {
4
+ const cmd =
5
+ process.platform === "darwin"
6
+ ? "open"
7
+ : process.platform === "win32"
8
+ ? "start"
9
+ : "xdg-open";
10
+ exec(`${cmd} "${url.toString()}"`);
11
+ }