@maker-or/opencms 0.1.0

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 (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.js +366 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # OpenCMS CLI
2
+
3
+ The developer-first CLI for creating and connecting Next.js applications to OpenCMS.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20 or newer
8
+ - Git
9
+
10
+ ## Usage
11
+
12
+ ```bash
13
+ npx @maker-or/opencms create
14
+ npx @maker-or/opencms login
15
+ npx @maker-or/opencms logout
16
+ npx @maker-or/opencms dev
17
+ npx @maker-or/opencms deploy
18
+ ```
19
+
20
+ `opencms create` authenticates you, creates an OpenCMS project, pulls the Next.js template, writes the project configuration, and installs dependencies.
21
+
22
+ The CLI stores its local login configuration in `~/.config/opencms/config.json` (or `$XDG_CONFIG_HOME/opencms/config.json` when configured).
package/dist/index.js ADDED
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createServer } from "node:http";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve } from "node:path";
7
+ import { access, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { constants } from "node:fs";
9
+ import { spawn } from "node:child_process";
10
+ import process from "node:process";
11
+
12
+ // ../../packages/sdk/src/index.ts
13
+ class OpenCmsApiError extends Error {
14
+ status;
15
+ constructor(message, status) {
16
+ super(message);
17
+ this.status = status;
18
+ this.name = "OpenCmsApiError";
19
+ }
20
+ }
21
+ function createSdk(options = {}) {
22
+ const baseUrl = (options.baseUrl ?? "http://localhost:3000").replace(/\/$/, "");
23
+ const fetcher = options.fetch ?? globalThis.fetch;
24
+ const projectId = options.projectId;
25
+ const environment = options.environment ?? "development";
26
+ async function request(path, init) {
27
+ const headers = new Headers(init?.headers);
28
+ headers.set("Accept", "application/json");
29
+ const token = await options.getToken?.();
30
+ if (token) {
31
+ headers.set("Authorization", `Bearer ${token}`);
32
+ }
33
+ const response = await fetcher(`${baseUrl}${path}`, {
34
+ ...init,
35
+ headers
36
+ });
37
+ if (!response.ok) {
38
+ let message = response.statusText || "OpenCMS API request failed";
39
+ try {
40
+ const body = await response.json();
41
+ message = body.error ?? message;
42
+ } catch {}
43
+ throw new OpenCmsApiError(message, response.status);
44
+ }
45
+ return await response.json();
46
+ }
47
+ return {
48
+ health: () => request("/api/health"),
49
+ projects: {
50
+ list: () => request("/api/projects"),
51
+ create: (input) => request("/api/projects", {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify(input)
55
+ })
56
+ },
57
+ pages: {
58
+ list: () => {
59
+ if (!projectId)
60
+ throw new Error("projectId is required to list pages");
61
+ return request(`/api/projects/${projectId}/pages?environment=${environment}`);
62
+ },
63
+ create: (input) => {
64
+ if (!projectId)
65
+ throw new Error("projectId is required to create pages");
66
+ return request(`/api/projects/${projectId}/pages`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ ...input, environment })
70
+ });
71
+ }
72
+ },
73
+ deploy: (targetProjectId = projectId) => {
74
+ if (!targetProjectId)
75
+ throw new Error("projectId is required to deploy");
76
+ return request(`/api/projects/${targetProjectId}/deploy`, {
77
+ method: "POST"
78
+ });
79
+ },
80
+ documents: {
81
+ list: () => {
82
+ if (!projectId)
83
+ throw new Error("projectId is required to list documents");
84
+ return request(`/api/projects/${projectId}/pages?environment=${environment}`);
85
+ },
86
+ create: (input) => request(`/api/projects/${projectId ?? ""}/pages`, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({ ...input, environment })
90
+ })
91
+ }
92
+ };
93
+ }
94
+
95
+ // src/index.ts
96
+ var dashboardUrl = process.env.OPENCMS_DASHBOARD_URL ?? "http://localhost:3000";
97
+ var apiUrl = process.env.OPENCMS_API_URL ?? "http://localhost:3000";
98
+ var configRoot = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
99
+ var configPath = join(configRoot, "opencms", "config.json");
100
+ async function readConfig() {
101
+ if (!await fileExists(configPath))
102
+ return {};
103
+ try {
104
+ return JSON.parse(await readFile(configPath, "utf8"));
105
+ } catch {
106
+ return {};
107
+ }
108
+ }
109
+ async function writeConfig(config) {
110
+ await mkdir(join(configRoot, "opencms"), { recursive: true });
111
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
112
+ `, "utf8");
113
+ }
114
+ function tokenFor(config) {
115
+ return process.env.OPENCMS_CLERK_TOKEN ?? config.token ?? null;
116
+ }
117
+ function sdk(config, projectId) {
118
+ return createSdk({
119
+ baseUrl: process.env.OPENCMS_API_URL ?? config.apiUrl ?? apiUrl,
120
+ projectId,
121
+ getToken: () => tokenFor(config)
122
+ });
123
+ }
124
+ function fileExists(path) {
125
+ return access(path, constants.F_OK).then(() => true, () => false);
126
+ }
127
+ function runCommand(command, args, options = {}) {
128
+ return new Promise((resolve2, reject) => {
129
+ const child = spawn(command, args, {
130
+ cwd: options.cwd,
131
+ env: options.env ?? process.env,
132
+ stdio: options.inherit ? "inherit" : "ignore"
133
+ });
134
+ child.once("error", reject);
135
+ child.once("exit", (code) => resolve2(code ?? 1));
136
+ });
137
+ }
138
+ async function openBrowser(url) {
139
+ const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
140
+ await runCommand(command[0], command.slice(1));
141
+ }
142
+ async function browserLogin() {
143
+ let resolveToken = () => {
144
+ return;
145
+ };
146
+ let rejectLogin = () => {
147
+ return;
148
+ };
149
+ const tokenPromise = new Promise((resolve2, reject) => {
150
+ resolveToken = resolve2;
151
+ rejectLogin = reject;
152
+ });
153
+ const server = createServer((request, response) => {
154
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
155
+ if (url.pathname !== "/callback") {
156
+ response.writeHead(200, { "Content-Type": "text/plain" });
157
+ response.end("Waiting for opencms login.");
158
+ return;
159
+ }
160
+ const token = url.searchParams.get("token");
161
+ if (!token) {
162
+ response.writeHead(400, { "Content-Type": "text/plain" });
163
+ response.end("Missing login token.");
164
+ return;
165
+ }
166
+ resolveToken(token);
167
+ response.writeHead(200, { "Content-Type": "text/html" });
168
+ response.end("<h1>OpenCMS login complete</h1><p>You can close this window.</p>");
169
+ });
170
+ await new Promise((resolve2, reject) => {
171
+ server.once("error", reject);
172
+ server.listen(0, "127.0.0.1", () => resolve2());
173
+ });
174
+ const address = server.address();
175
+ if (!address || typeof address === "string")
176
+ throw new Error("Unable to start the login callback server.");
177
+ const callback = `http://127.0.0.1:${address.port}/callback`;
178
+ const loginUrl = `${dashboardUrl.replace(/\/$/, "")}/cli/login?redirect_uri=${encodeURIComponent(callback)}`;
179
+ console.log(`Opening ${loginUrl}`);
180
+ try {
181
+ await openBrowser(loginUrl);
182
+ } catch {
183
+ console.log("Open the URL above in a browser to continue.");
184
+ }
185
+ const timeout = setTimeout(() => rejectLogin(new Error("Login timed out.")), 5 * 60 * 1000);
186
+ try {
187
+ return await tokenPromise;
188
+ } finally {
189
+ clearTimeout(timeout);
190
+ await new Promise((resolve2) => server.close(() => resolve2()));
191
+ }
192
+ }
193
+ async function ensureToken(config) {
194
+ const token = tokenFor(config);
195
+ if (token)
196
+ return token;
197
+ const loggedInToken = await browserLogin();
198
+ await writeConfig({ ...config, token: loggedInToken, apiUrl: config.apiUrl ?? apiUrl });
199
+ return loggedInToken;
200
+ }
201
+ async function login() {
202
+ const config = await readConfig();
203
+ if (process.env.OPENCMS_CLERK_TOKEN) {
204
+ await writeConfig({ ...config, token: process.env.OPENCMS_CLERK_TOKEN, apiUrl: config.apiUrl ?? apiUrl });
205
+ console.log("Saved OPENCMS_CLERK_TOKEN for local CLI use.");
206
+ return;
207
+ }
208
+ await ensureToken(config);
209
+ console.log("Logged in to OpenCMS.");
210
+ }
211
+ async function logout() {
212
+ const config = await readConfig();
213
+ const { token: _token, ...withoutToken } = config;
214
+ await writeConfig(withoutToken);
215
+ console.log("Logged out of OpenCMS.");
216
+ }
217
+ function slugify(value) {
218
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "project";
219
+ }
220
+ async function writeProjectEnv(destination, project, baseUrl) {
221
+ const envPath = join(destination, ".env.local");
222
+ const existing = await fileExists(envPath) ? await readFile(envPath, "utf8") : "";
223
+ const managedKeys = ["NEXT_PUBLIC_OPENCMS_PROJECT_ID", "OPENCMS_API_URL", "OPENCMS_ENVIRONMENT"];
224
+ const kept = existing.split(`
225
+ `).filter((line) => !managedKeys.some((key) => line.startsWith(`${key}=`))).join(`
226
+ `).trim();
227
+ const managed = [
228
+ `NEXT_PUBLIC_OPENCMS_PROJECT_ID=${project.id}`,
229
+ `OPENCMS_API_URL=${baseUrl}`,
230
+ "OPENCMS_ENVIRONMENT=development"
231
+ ].join(`
232
+ `);
233
+ await writeFile(envPath, `${kept ? `${kept}
234
+
235
+ ` : ""}${managed}
236
+ `, "utf8");
237
+ }
238
+ async function ensureCmsDirectory(destination, project, baseUrl) {
239
+ const cmsDirectory = join(destination, "cms");
240
+ await mkdir(cmsDirectory, { recursive: true });
241
+ const configFile = join(cmsDirectory, "opencms.ts");
242
+ if (!await fileExists(configFile)) {
243
+ await writeFile(configFile, `export const opencms = {
244
+ projectId: process.env.NEXT_PUBLIC_OPENCMS_PROJECT_ID ?? "${project.id}",
245
+ apiUrl: process.env.OPENCMS_API_URL ?? "${baseUrl}",
246
+ environment: process.env.OPENCMS_ENVIRONMENT ?? "development",
247
+ } as const;
248
+ `, "utf8");
249
+ }
250
+ }
251
+ async function packageManager(destination) {
252
+ const files = new Set(await readdir(destination));
253
+ if (files.has("pnpm-lock.yaml"))
254
+ return ["pnpm", "install"];
255
+ if (files.has("yarn.lock"))
256
+ return ["yarn", "install"];
257
+ if (files.has("package-lock.json"))
258
+ return ["npm", "install"];
259
+ return ["bun", "install"];
260
+ }
261
+ async function installDependencies(destination) {
262
+ if (process.env.OPENCMS_SKIP_INSTALL === "1")
263
+ return;
264
+ const manager = await packageManager(destination);
265
+ console.log(`Installing dependencies with ${manager[0]}…`);
266
+ if (await runCommand(manager[0], manager.slice(1), { cwd: destination, inherit: true }) !== 0) {
267
+ throw new Error("Dependency installation failed.");
268
+ }
269
+ }
270
+ async function pullTemplate(destination) {
271
+ const repository = process.env.OPENCMS_TEMPLATE_REPO ?? "https://github.com/opencms/template-nextjs.git";
272
+ if (await fileExists(destination))
273
+ throw new Error(`Destination already exists: ${destination}`);
274
+ console.log("Pulling the OpenCMS Next.js template…");
275
+ if (await runCommand("git", ["clone", "--depth", "1", repository, destination], { inherit: true }) !== 0) {
276
+ throw new Error("Unable to pull the Next.js template.");
277
+ }
278
+ await rm(join(destination, ".git"), { recursive: true, force: true });
279
+ }
280
+ async function createProject() {
281
+ const config = await readConfig();
282
+ await ensureToken(config);
283
+ const name = prompt("Project name:")?.trim();
284
+ if (!name)
285
+ throw new Error("A project name is required.");
286
+ const client = sdk(await readConfig());
287
+ const project = await client.projects.create({ name });
288
+ const destination = resolve(process.cwd(), slugify(project.name));
289
+ await pullTemplate(destination);
290
+ const baseUrl = process.env.OPENCMS_API_URL ?? config.apiUrl ?? apiUrl;
291
+ await writeProjectEnv(destination, project, baseUrl);
292
+ await ensureCmsDirectory(destination, project, baseUrl);
293
+ await installDependencies(destination);
294
+ await writeConfig({ ...await readConfig(), projectId: project.id, apiUrl: baseUrl });
295
+ console.log(`
296
+ Created ${project.name}.`);
297
+ console.log(`Project ID: ${project.id}`);
298
+ console.log(`Dashboard: ${dashboardUrl.replace(/\/$/, "")}/dashboard/${project.id}`);
299
+ console.log(`
300
+ Next steps:
301
+ cd ${slugify(project.name)}
302
+ npx @maker-or/opencms dev
303
+ npx @maker-or/opencms deploy`);
304
+ }
305
+ async function runDev() {
306
+ const config = await readConfig();
307
+ await ensureToken(config);
308
+ const manager = await packageManager(process.cwd());
309
+ const command = manager[0] === "npm" ? ["npm", "run", "dev"] : manager[0] === "pnpm" ? ["pnpm", "dev"] : manager[0] === "yarn" ? ["yarn", "dev"] : ["bun", "run", "dev"];
310
+ process.exit(await runCommand(command[0], command.slice(1), { cwd: process.cwd(), env: { ...process.env, OPENCMS_ENVIRONMENT: "development" }, inherit: true }));
311
+ }
312
+ function projectIdFromEnv() {
313
+ const envPath = join(process.cwd(), ".env.local");
314
+ return fileExists(envPath).then(async (exists) => {
315
+ if (!exists)
316
+ return;
317
+ const match = (await readFile(envPath, "utf8")).match(/^NEXT_PUBLIC_OPENCMS_PROJECT_ID=(.+)$/m);
318
+ return match?.[1]?.trim();
319
+ });
320
+ }
321
+ async function deploy() {
322
+ const config = await readConfig();
323
+ await ensureToken(config);
324
+ const projectId = await projectIdFromEnv() ?? config.projectId;
325
+ if (!projectId)
326
+ throw new Error("No OpenCMS project is configured in this directory.");
327
+ const deployment = await sdk(await readConfig()).deploy(projectId);
328
+ console.log(`Deployed ${deployment.sourceEnvironment} content to ${deployment.targetEnvironment}.`);
329
+ if (process.env.VERCEL_TOKEN) {
330
+ console.log("Deploying the application to Vercel…");
331
+ if (await runCommand("npx", ["vercel", "--prod", "--yes", "--token", process.env.VERCEL_TOKEN], { cwd: process.cwd(), inherit: true }) !== 0) {
332
+ throw new Error("Vercel deployment failed.");
333
+ }
334
+ }
335
+ }
336
+ function printHelp() {
337
+ console.log(`OpenCMS CLI
338
+
339
+ Usage: npx @maker-or/opencms <command>
340
+
341
+ Commands:
342
+ create Create a project and connected Next.js app
343
+ login Authenticate this machine
344
+ logout Remove local credentials
345
+ dev Run the local Next.js app against development
346
+ deploy Promote development content to production`);
347
+ }
348
+ var command = process.argv[2];
349
+ try {
350
+ if (command === "create")
351
+ await createProject();
352
+ else if (command === "login")
353
+ await login();
354
+ else if (command === "logout")
355
+ await logout();
356
+ else if (command === "dev")
357
+ await runDev();
358
+ else if (command === "deploy")
359
+ await deploy();
360
+ else
361
+ printHelp();
362
+ } catch (error) {
363
+ console.error(`
364
+ OpenCMS: ${error instanceof Error ? error.message : String(error)}`);
365
+ process.exitCode = 1;
366
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@maker-or/opencms",
3
+ "version": "0.1.0",
4
+ "description": "The developer-first CLI for OpenCMS",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/maker-or/openCMS.git",
9
+ "directory": "apps/cli"
10
+ },
11
+ "homepage": "https://github.com/maker-or/openCMS",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "opencms",
17
+ "cms",
18
+ "nextjs",
19
+ "cli"
20
+ ],
21
+ "bin": {
22
+ "opencms": "dist/index.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "bun build src/index.ts --outfile dist/index.js --target=node --format=esm",
29
+ "dev": "bun --watch src/index.ts",
30
+ "start": "node dist/index.js",
31
+ "typecheck": "tsc --noEmit",
32
+ "prepack": "bun run build"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.13.10",
39
+ "typescript": "^5.8.3"
40
+ }
41
+ }