@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.
- package/README.md +2 -0
- package/dist/index.js +102 -35
- 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
|
-
|
|
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, {
|
|
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
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|