@maker-or/opencms 0.1.7 → 0.1.8

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 +2 -0
  2. package/dist/index.js +102 -35
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -23,6 +23,8 @@ npx @maker-or/opencms deploy
23
23
 
24
24
  The generated `cms/schema.json` file defines the project's content types and allowed blocks. The CLI syncs it to the development environment when `dev` or `deploy` runs.
25
25
 
26
+ `opencms deploy` promotes the development schema and only published development pages as one production snapshot. Draft pages stay out of production and production pages no longer present in the published development snapshot are removed. When `VERCEL_TOKEN` is set, the command also deploys the application with its project ID, API origin, and production CMS environment supplied to Vercel at build time and runtime.
27
+
26
28
  The CLI stores its local login configuration in `~/.config/opencms/config.json` (or `$XDG_CONFIG_HOME/opencms/config.json` when configured).
27
29
 
28
30
  The CLI uses the OpenCMS control-plane origin from `OPENCMS_URL`. Set it to the dashboard/API origin for your hosted, local, or self-hosted instance. `OPENCMS_API_URL` and `OPENCMS_DASHBOARD_URL` remain supported as separate legacy overrides, but there is no baked-in deployment URL.
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createServer } from "node:http";
5
+ import { randomUUID } from "node:crypto";
5
6
  import { homedir } from "node:os";
6
7
  import { join, resolve } from "node:path";
7
- import { access, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { access, chmod, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
8
9
  import { constants } from "node:fs";
9
10
  import { spawn } from "node:child_process";
10
11
  import { createInterface } from "node:readline/promises";
@@ -103,18 +104,21 @@ function createSdk(options = {}) {
103
104
  method: "POST",
104
105
  headers: { "Content-Type": "application/json" },
105
106
  body: JSON.stringify(input)
107
+ }),
108
+ delete: (targetProjectId) => request(`/api/projects/${targetProjectId}`, {
109
+ method: "DELETE"
106
110
  })
107
111
  },
108
112
  schema: {
109
113
  get: () => {
110
114
  if (!projectId)
111
115
  throw new Error("projectId is required to get the schema");
112
- return request(`/api/projects/${projectId}/schema`);
116
+ return request(`/api/projects/${projectId}/schema?environment=${environment}`);
113
117
  },
114
118
  update: (schema) => {
115
119
  if (!projectId)
116
120
  throw new Error("projectId is required to update the schema");
117
- return request(`/api/projects/${projectId}/schema`, {
121
+ return request(`/api/projects/${projectId}/schema?environment=${environment}`, {
118
122
  method: "PUT",
119
123
  headers: { "Content-Type": "application/json" },
120
124
  body: JSON.stringify(schema)
@@ -205,6 +209,27 @@ function createSdk(options = {}) {
205
209
  };
206
210
  }
207
211
 
212
+ // src/vercel.ts
213
+ function vercelDeploymentArgs({
214
+ apiUrl,
215
+ projectId,
216
+ token
217
+ }) {
218
+ const connectionVariables = [
219
+ `NEXT_PUBLIC_OPENCMS_PROJECT_ID=${projectId}`,
220
+ `OPENCMS_API_URL=${apiUrl}`,
221
+ "OPENCMS_ENVIRONMENT=production"
222
+ ];
223
+ return [
224
+ "vercel",
225
+ "--prod",
226
+ "--yes",
227
+ "--token",
228
+ token,
229
+ ...connectionVariables.flatMap((value) => ["--build-env", value, "--env", value])
230
+ ];
231
+ }
232
+
208
233
  // src/index.ts
209
234
  var configRoot = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
210
235
  var configPath = join(configRoot, "opencms", "config.json");
@@ -218,9 +243,14 @@ async function readConfig() {
218
243
  }
219
244
  }
220
245
  async function writeConfig(config) {
221
- await mkdir(join(configRoot, "opencms"), { recursive: true });
246
+ const directory = join(configRoot, "opencms");
247
+ await mkdir(directory, { recursive: true, mode: 448 });
222
248
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}
223
- `, "utf8");
249
+ `, { encoding: "utf8", mode: 384 });
250
+ if (process.platform !== "win32") {
251
+ await chmod(directory, 448);
252
+ await chmod(configPath, 384);
253
+ }
224
254
  }
225
255
  function tokenFor(config) {
226
256
  return process.env.OPENCMS_CLERK_TOKEN ?? config.token ?? null;
@@ -272,6 +302,7 @@ async function openBrowser(url) {
272
302
  await runCommand(command[0], command.slice(1));
273
303
  }
274
304
  async function browserLogin(config) {
305
+ const loginState = randomUUID();
275
306
  let resolveToken = () => {
276
307
  return;
277
308
  };
@@ -289,14 +320,24 @@ async function browserLogin(config) {
289
320
  response.end("Waiting for opencms login.");
290
321
  return;
291
322
  }
323
+ if (url.searchParams.get("state") !== loginState) {
324
+ response.writeHead(400, { "Content-Type": "text/plain", "Cache-Control": "no-store" });
325
+ response.end("Invalid login state.");
326
+ return;
327
+ }
292
328
  const token = url.searchParams.get("token");
293
329
  if (!token) {
294
- response.writeHead(400, { "Content-Type": "text/plain" });
330
+ response.writeHead(400, { "Content-Type": "text/plain", "Cache-Control": "no-store" });
295
331
  response.end("Missing login token.");
296
332
  return;
297
333
  }
298
334
  resolveToken(token);
299
- response.writeHead(200, { "Content-Type": "text/html" });
335
+ response.writeHead(200, {
336
+ "Content-Type": "text/html",
337
+ "Cache-Control": "no-store",
338
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
339
+ "X-Content-Type-Options": "nosniff"
340
+ });
300
341
  response.end("<h1>OpenCMS login complete</h1><p>You can close this window.</p>");
301
342
  });
302
343
  await new Promise((resolve2, reject) => {
@@ -307,7 +348,7 @@ async function browserLogin(config) {
307
348
  if (!address || typeof address === "string")
308
349
  throw new Error("Unable to start the login callback server.");
309
350
  const callback = `http://127.0.0.1:${address.port}/callback`;
310
- const loginUrl = `${dashboardUrlFor(config)}/cli/login?redirect_uri=${encodeURIComponent(callback)}`;
351
+ const loginUrl = `${dashboardUrlFor(config)}/cli/login?redirect_uri=${encodeURIComponent(callback)}&state=${encodeURIComponent(loginState)}`;
311
352
  console.log(`Opening ${loginUrl}`);
312
353
  try {
313
354
  await openBrowser(loginUrl);
@@ -339,6 +380,18 @@ async function reauthenticate(config) {
339
380
  await writeConfig({ ...config, token: loggedInToken, apiUrl: apiUrlFor(config) });
340
381
  return loggedInToken;
341
382
  }
383
+ async function withReauthentication(operation) {
384
+ let config = await readConfig();
385
+ try {
386
+ return await operation(config);
387
+ } catch (error) {
388
+ if (!(error instanceof OpenCmsApiError) || error.status !== 401)
389
+ throw error;
390
+ await reauthenticate(config);
391
+ config = await readConfig();
392
+ return operation(config);
393
+ }
394
+ }
342
395
  async function login() {
343
396
  const config = await readConfig();
344
397
  if (process.env.OPENCMS_CLERK_TOKEN) {
@@ -352,6 +405,18 @@ async function login() {
352
405
  }
353
406
  async function logout() {
354
407
  const config = await readConfig();
408
+ if (config.token?.startsWith("ocms_")) {
409
+ try {
410
+ const response = await fetch(`${apiUrlFor(config)}/api/cli/tokens/current`, {
411
+ method: "DELETE",
412
+ headers: { Authorization: `Bearer ${config.token}` }
413
+ });
414
+ if (!response.ok)
415
+ console.warn("OpenCMS: The remote CLI session could not be revoked.");
416
+ } catch {
417
+ console.warn("OpenCMS: The remote CLI session could not be revoked.");
418
+ }
419
+ }
355
420
  const { token: _token, ...withoutToken } = config;
356
421
  await writeConfig(withoutToken);
357
422
  console.log("Logged out of OpenCMS.");
@@ -430,6 +495,9 @@ async function createProject() {
430
495
  const name = await ask("Project name: ");
431
496
  if (!name)
432
497
  throw new Error("A project name is required.");
498
+ const destination = resolve(process.cwd(), slugify(name));
499
+ if (await fileExists(destination))
500
+ throw new Error(`Destination already exists: ${destination}`);
433
501
  let currentConfig = await readConfig();
434
502
  let client = sdk(currentConfig);
435
503
  let project;
@@ -443,12 +511,21 @@ async function createProject() {
443
511
  client = sdk(currentConfig);
444
512
  project = await client.projects.create({ name });
445
513
  }
446
- const destination = resolve(process.cwd(), slugify(project.name));
447
- await pullTemplate(destination);
448
- const baseUrl = apiUrlFor(config);
449
- await writeProjectEnv(destination, project, baseUrl);
450
- await ensureCmsDirectory(destination, project, baseUrl);
451
- await installDependencies(destination);
514
+ const baseUrl = apiUrlFor(currentConfig);
515
+ try {
516
+ await pullTemplate(destination);
517
+ await writeProjectEnv(destination, project, baseUrl);
518
+ await ensureCmsDirectory(destination, project, baseUrl);
519
+ await installDependencies(destination);
520
+ } catch (error) {
521
+ await rm(destination, { recursive: true, force: true });
522
+ try {
523
+ await client.projects.delete(project.id);
524
+ } catch {
525
+ console.error(`OpenCMS: Local setup failed and cloud rollback also failed. Project ID: ${project.id}`);
526
+ }
527
+ throw error;
528
+ }
452
529
  await writeConfig({ ...await readConfig(), projectId: project.id, apiUrl: baseUrl });
453
530
  console.log(`
454
531
  Created ${project.name}.`);
@@ -463,14 +540,8 @@ Next steps:
463
540
  async function runDev() {
464
541
  const config = await readConfig();
465
542
  await ensureToken(config);
466
- try {
467
- await syncLocalSchema(await projectIdFromEnv(), await readConfig());
468
- } catch (error) {
469
- if (!(error instanceof OpenCmsApiError) || error.status !== 401)
470
- throw error;
471
- await reauthenticate(await readConfig());
472
- await syncLocalSchema(await projectIdFromEnv(), await readConfig());
473
- }
543
+ const projectId = await projectIdFromEnv();
544
+ await withReauthentication((currentConfig) => syncLocalSchema(projectId, currentConfig));
474
545
  const manager = await packageManager(process.cwd());
475
546
  const script = await nextDevScript(process.cwd());
476
547
  const command = manager[0] === "npm" ? ["npm", "run", script] : manager[0] === "pnpm" ? ["pnpm", script] : manager[0] === "yarn" ? ["yarn", script] : ["bun", "run", script];
@@ -532,24 +603,20 @@ async function deploy() {
532
603
  const projectId = await projectIdFromEnv() ?? config.projectId;
533
604
  if (!projectId)
534
605
  throw new Error("No OpenCMS project is configured in this directory.");
535
- let deployment;
536
- try {
537
- await syncLocalSchema(projectId, await readConfig());
538
- deployment = await sdk(await readConfig()).deploy(projectId);
539
- } catch (error) {
540
- if (!(error instanceof OpenCmsApiError) || error.status !== 401)
541
- throw error;
542
- await reauthenticate(await readConfig());
543
- await syncLocalSchema(projectId, await readConfig());
544
- deployment = await sdk(await readConfig()).deploy(projectId);
545
- }
546
- console.log(`Deployed ${deployment.sourceEnvironment} content to ${deployment.targetEnvironment}.`);
606
+ await withReauthentication((currentConfig) => syncLocalSchema(projectId, currentConfig));
547
607
  if (process.env.VERCEL_TOKEN) {
548
608
  console.log("Deploying the application to Vercel…");
549
- if (await runCommand("npx", ["vercel", "--prod", "--yes", "--token", process.env.VERCEL_TOKEN], { cwd: process.cwd(), inherit: true }) !== 0) {
609
+ const args = vercelDeploymentArgs({
610
+ apiUrl: apiUrlFor(await readConfig()),
611
+ projectId,
612
+ token: process.env.VERCEL_TOKEN
613
+ });
614
+ if (await runCommand("npx", args, { cwd: process.cwd(), inherit: true }) !== 0) {
550
615
  throw new Error("Vercel deployment failed.");
551
616
  }
552
617
  }
618
+ const deployment = await withReauthentication((currentConfig) => sdk(currentConfig).deploy(projectId));
619
+ console.log(`Deployed ${deployment.sourceEnvironment} content to ${deployment.targetEnvironment}.`);
553
620
  }
554
621
  function printHelp() {
555
622
  console.log(`OpenCMS CLI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maker-or/opencms",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "The developer-first CLI for OpenCMS",
5
5
  "type": "module",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "build": "bun build src/index.ts --outfile dist/index.js --target=node --format=esm",
29
29
  "dev": "bun --watch src/index.ts",
30
30
  "start": "node dist/index.js",
31
+ "test": "bun run build && bun test",
31
32
  "typecheck": "tsc --noEmit",
32
33
  "prepack": "bun run build"
33
34
  },
@@ -35,6 +36,7 @@
35
36
  "node": ">=20"
36
37
  },
37
38
  "devDependencies": {
39
+ "@types/bun": "latest",
38
40
  "@types/node": "^22.13.10",
39
41
  "typescript": "^5.8.3"
40
42
  }