@open-press/core 0.8.0 → 1.0.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 (64) hide show
  1. package/README.md +6 -3
  2. package/engine/cli.mjs +8 -8
  3. package/engine/commands/_shared.mjs +37 -15
  4. package/engine/commands/image.mjs +29 -0
  5. package/engine/commands/skills-sync.mjs +71 -0
  6. package/engine/commands/typecheck.mjs +63 -1
  7. package/engine/commands/upgrade.mjs +3 -3
  8. package/engine/document-export.mjs +1 -1
  9. package/engine/output/chrome-pdf.mjs +92 -0
  10. package/engine/output/static-server.mjs +48 -9
  11. package/engine/react/comment-marker.mjs +13 -13
  12. package/engine/react/document-entry.mjs +35 -28
  13. package/engine/react/document-export.mjs +309 -170
  14. package/engine/react/mdx-compile.mjs +30 -0
  15. package/engine/react/measurement-css.mjs +21 -0
  16. package/engine/react/object-entities.mjs +85 -0
  17. package/engine/react/pagination/allocator.mjs +48 -3
  18. package/engine/react/pagination.mjs +1 -1
  19. package/engine/react/pipeline/allocate.mjs +31 -65
  20. package/engine/react/pipeline/frame-measurement.mjs +4 -0
  21. package/engine/react/press-tree-inspection.mjs +172 -0
  22. package/engine/react/sources/mdx-resolver.mjs +1 -1
  23. package/engine/react/style-discovery.mjs +22 -4
  24. package/engine/runtime/config.d.mts +8 -0
  25. package/engine/runtime/config.mjs +57 -60
  26. package/engine/runtime/file-utils.mjs +9 -1
  27. package/engine/runtime/page-geometry.mjs +131 -0
  28. package/engine/runtime/source-text-tools.mjs +1 -1
  29. package/engine/runtime/source-workspace.mjs +12 -3
  30. package/engine/runtime/validation.mjs +19 -10
  31. package/package.json +3 -5
  32. package/src/openpress/app/OpenPressApp.tsx +173 -17
  33. package/src/openpress/app/OpenPressRuntime.tsx +10 -2
  34. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  35. package/src/openpress/core/Frame.tsx +20 -7
  36. package/src/openpress/core/FrameContext.tsx +2 -0
  37. package/src/openpress/core/Press.tsx +25 -4
  38. package/src/openpress/core/Workspace.tsx +36 -0
  39. package/src/openpress/core/index.tsx +10 -3
  40. package/src/openpress/core/primitives.tsx +48 -1
  41. package/src/openpress/core/types.ts +86 -41
  42. package/src/openpress/core/useSource.ts +1 -1
  43. package/src/openpress/document-model/documentTypes.ts +9 -0
  44. package/src/openpress/document-model/index.ts +1 -0
  45. package/src/openpress/document-model/objectEntityModel.ts +4 -0
  46. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  47. package/src/openpress/mdx/index.ts +15 -7
  48. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  49. package/src/openpress/reader/index.ts +1 -0
  50. package/src/openpress/workbench/Workbench.tsx +120 -21
  51. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  52. package/src/openpress/workbench/actions/SearchControl.tsx +3 -3
  53. package/src/openpress/workbench/actions/index.ts +1 -0
  54. package/src/openpress/workbench/inspector/useInspectorComments.ts +7 -1
  55. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +5 -1
  56. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +4 -2
  57. package/src/openpress/workbench/workbenchFormatters.ts +2 -2
  58. package/src/styles/openpress/reader-runtime.css +9 -0
  59. package/src/styles/openpress/workbench-panels.css +113 -0
  60. package/src/styles/openpress/workspace-gallery.css +300 -0
  61. package/src/styles/openpress.css +1 -0
  62. package/tsconfig.json +1 -1
  63. package/engine/commands/init.mjs +0 -24
  64. package/engine/init.mjs +0 -90
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { pathToFileURL } from "node:url";
3
+ import { normalizePageGeometry } from "./page-geometry.mjs";
4
4
 
5
5
  const DEFAULT_CONFIG = {
6
6
  title: "OpenPress Document",
@@ -30,33 +30,72 @@ const DEFAULT_CONFIG = {
30
30
  commitDirty: false,
31
31
  requiresConfirmation: true,
32
32
  },
33
+ page: null,
33
34
  };
34
35
 
36
+ // 1.0 contract: the only user-writable config lives in package.json
37
+ // under the "openpress" field. The engine reads it synchronously so
38
+ // the deploy command can resolve its adapter before any React render.
39
+ //
40
+ // Everything else is convention (path layout) or declared on
41
+ // <Press> / <Workspace> JSX props (document metadata).
35
42
  export async function loadConfig(root = ".") {
36
43
  const workspaceRoot = path.resolve(root);
37
- const configPath = path.join(workspaceRoot, "openpress.config.mjs");
38
- const rootConfig = await readUserConfig(configPath);
39
- const { config, sourceConfigPath } = await resolveUserConfig(workspaceRoot, rootConfig, configPath);
40
- return normalizeConfig(workspaceRoot, config, sourceConfigPath);
44
+ const packageOpenpress = await readPackageOpenpressField(workspaceRoot);
45
+ return normalizeConfig(workspaceRoot, packageOpenpress ?? {});
41
46
  }
42
47
 
43
- export function normalizeConfig(root, userConfig = {}, configPath = path.join(root, "openpress.config.mjs")) {
48
+ async function readPackageOpenpressField(workspaceRoot) {
49
+ const pkgPath = path.join(workspaceRoot, "package.json");
50
+ try {
51
+ const raw = await fs.readFile(pkgPath, "utf8");
52
+ const parsed = JSON.parse(raw);
53
+ const field = parsed?.openpress;
54
+ return (field && typeof field === "object" && !Array.isArray(field)) ? field : null;
55
+ } catch (error) {
56
+ if (error?.code === "ENOENT") return null;
57
+ if (error instanceof SyntaxError) {
58
+ throw new Error(`Malformed package.json at ${pkgPath}: ${error.message}`);
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ // Convention-only fields. The user can't override these — they're part
65
+ // of OpenPress's product surface. If you don't like the layout, your
66
+ // project isn't an OpenPress workspace.
67
+ const CONVENTION = {
68
+ documentDir: "press",
69
+ sourceDir: "chapters",
70
+ mediaDir: "media",
71
+ themeDir: "theme",
72
+ componentsDir: "components",
73
+ designDoc: "design.md",
74
+ publicDir: "public/openpress",
75
+ outputDir: "dist-react",
76
+ };
77
+
78
+ export function normalizeConfig(root, userConfig = {}, configPath = path.join(root, "package.json")) {
44
79
  const config = {
45
80
  root: path.resolve(root),
46
81
  configPath,
47
- title: stringValue(userConfig.title, DEFAULT_CONFIG.title),
48
- subtitle: optionalStringValue(userConfig.subtitle, DEFAULT_CONFIG.subtitle) ?? "",
49
- organization: optionalStringValue(userConfig.organization, DEFAULT_CONFIG.organization) ?? "",
50
- workspaceLabel: optionalStringValue(userConfig.workspaceLabel, DEFAULT_CONFIG.workspaceLabel) ?? "",
51
- documentDir: documentPathValue(userConfig.documentDir, DEFAULT_CONFIG.documentDir),
52
- sourceDir: relativePathValue(userConfig.sourceDir, DEFAULT_CONFIG.sourceDir),
53
- mediaDir: relativePathValue(userConfig.mediaDir, DEFAULT_CONFIG.mediaDir),
54
- themeDir: relativePathValue(userConfig.themeDir, DEFAULT_CONFIG.themeDir),
55
- designDoc: relativePathValue(userConfig.designDoc, DEFAULT_CONFIG.designDoc),
56
- componentsDir: relativePathValue(userConfig.componentsDir, DEFAULT_CONFIG.componentsDir),
57
- publicDir: relativePathValue(userConfig.publicDir, DEFAULT_CONFIG.publicDir),
58
- outputDir: relativePathValue(userConfig.outputDir, DEFAULT_CONFIG.outputDir),
82
+ // Document metadata defaults — actual values are merged in by
83
+ // loadReactDocumentEntry from <Press> / <Workspace> JSX props.
84
+ title: DEFAULT_CONFIG.title,
85
+ subtitle: "",
86
+ organization: "",
87
+ workspaceLabel: "",
88
+ // Paths — fixed conventions, not user-configurable.
89
+ documentDir: CONVENTION.documentDir,
90
+ sourceDir: CONVENTION.sourceDir,
91
+ mediaDir: CONVENTION.mediaDir,
92
+ themeDir: CONVENTION.themeDir,
93
+ designDoc: CONVENTION.designDoc,
94
+ componentsDir: CONVENTION.componentsDir,
95
+ publicDir: CONVENTION.publicDir,
96
+ outputDir: CONVENTION.outputDir,
59
97
  captionNumbering: captionNumberingValue(userConfig.captionNumbering, DEFAULT_CONFIG.captionNumbering),
98
+ page: normalizePageGeometry(userConfig.page ?? DEFAULT_CONFIG.page),
60
99
  pdf: {
61
100
  filename: fileNameValue(userConfig.pdf?.filename, DEFAULT_CONFIG.pdf.filename),
62
101
  },
@@ -91,32 +130,6 @@ export function publicPdfHref(config) {
91
130
  return `/${config.pdf.filename}`;
92
131
  }
93
132
 
94
- async function readUserConfig(configPath) {
95
- try {
96
- const stat = await fs.stat(configPath);
97
- const configUrl = `${pathToFileURL(configPath).href}?mtime=${stat.mtimeMs}`;
98
- const mod = await import(configUrl);
99
- return mod.default ?? {};
100
- } catch (error) {
101
- if (error?.code === "ENOENT") return {};
102
- throw error;
103
- }
104
- }
105
-
106
- async function resolveUserConfig(root, rootConfig, configPath) {
107
- const documentConfigPath = configPathValue(rootConfig.config ?? rootConfig.documentConfig);
108
- if (!documentConfigPath) {
109
- return { config: rootConfig, sourceConfigPath: configPath };
110
- }
111
-
112
- const sourceConfigPath = path.resolve(root, documentConfigPath);
113
- const documentConfig = await readUserConfig(sourceConfigPath);
114
- return {
115
- config: { ...documentConfig, ...rootConfig },
116
- sourceConfigPath,
117
- };
118
- }
119
-
120
133
  function stringValue(value, fallback) {
121
134
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
122
135
  }
@@ -148,22 +161,6 @@ function fileNameValue(value, fallback) {
148
161
  return fileName;
149
162
  }
150
163
 
151
- function configPathValue(value) {
152
- if (typeof value !== "string" || !value.trim()) return null;
153
- return relativePathValue(value, null);
154
- }
155
-
156
- function documentPathValue(value, fallback) {
157
- const raw = stringValue(value, fallback).replaceAll("\\", "/");
158
- if (path.isAbsolute(raw)) throw new Error(`OpenPress config paths must be relative, got: ${raw}`);
159
- const normalized = path.posix.normalize(raw).replace(/^\.\//, "");
160
- if (normalized === ".") return ".";
161
- if (!normalized || normalized === ".." || normalized.startsWith("../")) {
162
- throw new Error(`OpenPress config path escapes workspace: ${raw}`);
163
- }
164
- return normalized;
165
- }
166
-
167
164
  function relativePathValue(value, fallback) {
168
165
  const raw = stringValue(value, fallback).replaceAll("\\", "/");
169
166
  if (path.isAbsolute(raw)) throw new Error(`OpenPress config paths must be relative, got: ${raw}`);
@@ -10,6 +10,7 @@ const CONTENT_CSS_LAYERS = [
10
10
  { path: "page-surfaces/chapter-opener.css", optional: true },
11
11
  "page-surfaces/back-cover.css",
12
12
  "page-surfaces/toc.css",
13
+ { path: "page-surfaces", type: "directory", exclude: ["cover.css", "chapter-opener.css", "back-cover.css", "toc.css"] },
13
14
  "shell/reader-controls.css",
14
15
  "base/print.css",
15
16
  ];
@@ -32,6 +33,12 @@ export async function buildContentCss(root, config) {
32
33
  const contentAssetsDir = config.paths.themeDir;
33
34
  const parts = [];
34
35
  for (const layer of CONTENT_CSS_LAYERS) {
36
+ if (typeof layer !== "string" && layer.type === "directory") {
37
+ await appendCssDirectory(parts, path.join(contentAssetsDir, layer.path), layer.path, {
38
+ exclude: new Set(layer.exclude ?? []),
39
+ });
40
+ continue;
41
+ }
35
42
  const relativePath = typeof layer === "string" ? layer : layer.path;
36
43
  const cssPath = path.join(contentAssetsDir, relativePath);
37
44
  let css;
@@ -66,7 +73,7 @@ export async function buildComponentsCss(root, config) {
66
73
  return parts.join("");
67
74
  }
68
75
 
69
- async function appendCssDirectory(parts, directory, labelPrefix) {
76
+ async function appendCssDirectory(parts, directory, labelPrefix, options = {}) {
70
77
  let entries;
71
78
  try {
72
79
  entries = await fs.readdir(directory);
@@ -74,6 +81,7 @@ async function appendCssDirectory(parts, directory, labelPrefix) {
74
81
  return;
75
82
  }
76
83
  for (const name of entries.filter((entry) => entry.endsWith(".css")).sort()) {
84
+ if (options.exclude?.has(name)) continue;
77
85
  parts.push(`/* === ${labelPrefix}/${name} === */\n`);
78
86
  parts.push((await fs.readFile(path.join(directory, name), "utf8")).trimEnd());
79
87
  parts.push("\n\n");
@@ -0,0 +1,131 @@
1
+ const CSS_LENGTH_RE = /^(\d+(?:\.\d+)?)(px|mm|cm|in|pt|pc)$/i;
2
+
3
+ export const PAGE_GEOMETRY_PRESETS = {
4
+ a4: {
5
+ id: "a4",
6
+ label: "A4 Page",
7
+ width: "210mm",
8
+ height: "297mm",
9
+ },
10
+ "social-square": {
11
+ id: "social-square",
12
+ label: "Social Square",
13
+ width: "1080px",
14
+ height: "1080px",
15
+ },
16
+ "slide-16-9": {
17
+ id: "slide-16-9",
18
+ label: "Slide 16:9",
19
+ width: "1920px",
20
+ height: "1080px",
21
+ },
22
+ };
23
+
24
+ export function normalizePageGeometry(value) {
25
+ if (value == null || value === false) return null;
26
+
27
+ if (typeof value === "string") {
28
+ return presetPageGeometry(value);
29
+ }
30
+
31
+ if (typeof value !== "object" || Array.isArray(value)) {
32
+ throw new Error("OpenPress config page must be a preset name or an object.");
33
+ }
34
+
35
+ const preset = typeof value.preset === "string" ? presetPageGeometry(value.preset) : null;
36
+ const width = cssLengthValue(value.width, preset?.width);
37
+ const height = cssLengthValue(value.height, preset?.height);
38
+ if (!width || !height) {
39
+ throw new Error("OpenPress config page requires width and height when no preset is provided.");
40
+ }
41
+
42
+ const id = optionalId(value.id, preset?.id ?? "custom");
43
+ const label = optionalLabel(value.label, preset?.label ?? "Custom Page");
44
+
45
+ return pageGeometry({
46
+ id,
47
+ label,
48
+ width,
49
+ height,
50
+ });
51
+ }
52
+
53
+ export function pageGeometryToTheme(page) {
54
+ if (!page) return undefined;
55
+ return {
56
+ pagePreset: page.id,
57
+ pageLabel: page.label,
58
+ pageWidth: page.width,
59
+ pageHeight: page.height,
60
+ pageAspectRatio: page.aspectRatio,
61
+ pageHeightRatio: page.heightRatio,
62
+ };
63
+ }
64
+
65
+ function presetPageGeometry(id) {
66
+ const preset = PAGE_GEOMETRY_PRESETS[id];
67
+ if (!preset) {
68
+ throw new Error(
69
+ `Unknown OpenPress page preset: "${id}". ` +
70
+ `Available presets: ${Object.keys(PAGE_GEOMETRY_PRESETS).join(", ")}.`,
71
+ );
72
+ }
73
+ return pageGeometry(preset);
74
+ }
75
+
76
+ function pageGeometry({ id, label, width, height }) {
77
+ const ratio = sameUnitRatio(width, height);
78
+ return {
79
+ id,
80
+ label,
81
+ width,
82
+ height,
83
+ aspectRatio: ratio ? `${trimNumber(ratio.width)} / ${trimNumber(ratio.height)}` : undefined,
84
+ heightRatio: ratio ? trimNumber(ratio.height / ratio.width) : undefined,
85
+ };
86
+ }
87
+
88
+ function cssLengthValue(value, fallback) {
89
+ if (value == null) return fallback ?? null;
90
+ if (typeof value !== "string" || !value.trim()) {
91
+ throw new Error("OpenPress page width/height must be CSS length strings.");
92
+ }
93
+ const trimmed = value.trim();
94
+ if (!CSS_LENGTH_RE.test(trimmed)) {
95
+ throw new Error(`OpenPress page size must be an absolute CSS length, got: ${trimmed}`);
96
+ }
97
+ return trimmed;
98
+ }
99
+
100
+ function optionalId(value, fallback) {
101
+ if (value == null) return fallback;
102
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/i.test(value)) {
103
+ throw new Error("OpenPress page id must be a simple slug.");
104
+ }
105
+ return value;
106
+ }
107
+
108
+ function optionalLabel(value, fallback) {
109
+ if (value == null) return fallback;
110
+ if (typeof value !== "string" || !value.trim()) {
111
+ throw new Error("OpenPress page label must be a non-empty string.");
112
+ }
113
+ return value.trim();
114
+ }
115
+
116
+ function sameUnitRatio(width, height) {
117
+ const w = parseCssLength(width);
118
+ const h = parseCssLength(height);
119
+ if (!w || !h || w.unit.toLowerCase() !== h.unit.toLowerCase()) return null;
120
+ return { width: w.value, height: h.value };
121
+ }
122
+
123
+ function parseCssLength(value) {
124
+ const match = CSS_LENGTH_RE.exec(value);
125
+ if (!match) return null;
126
+ return { value: Number(match[1]), unit: match[2] };
127
+ }
128
+
129
+ function trimNumber(value) {
130
+ return Number(value.toFixed(6)).toString();
131
+ }
@@ -824,7 +824,7 @@ function normalizeSourceTextPath(value) {
824
824
  return String(value ?? "")
825
825
  .replaceAll("\\", "/")
826
826
  .replace(/^\.\//, "")
827
- .replace(/^document\//, "");
827
+ .replace(/^press\//, "");
828
828
  }
829
829
 
830
830
  function stringValue(value) {
@@ -12,10 +12,19 @@ export async function resolveActiveSourceWorkspace(config) {
12
12
  const reactEntry = await loadReactDocumentEntry(config.root);
13
13
  if (!reactEntry) {
14
14
  throw new Error(
15
- "React/MDX document entry not found. Expected document/index.tsx with a Press default export before using workspace source tools.",
15
+ "React/MDX document entry not found. Expected press/index.tsx with a Press default export before using workspace source tools.",
16
16
  );
17
17
  }
18
- const contentRoots = contentRootsFromSources(reactEntry.sources, reactEntry.config);
18
+ // Aggregate sources across every Press in the Workspace. Workspace
19
+ // tooling (validate, search, replace, inspect) walks the union — a
20
+ // multi-Press project's sources all live under the same press/ tree.
21
+ const aggregateSources = {};
22
+ for (const press of reactEntry.presses ?? []) {
23
+ if (press.sources && typeof press.sources === "object") {
24
+ Object.assign(aggregateSources, press.sources);
25
+ }
26
+ }
27
+ const contentRoots = contentRootsFromSources(aggregateSources, reactEntry.config);
19
28
  const sourceDir = firstDirectoryRoot(contentRoots) ?? reactEntry.config.paths.documentRoot;
20
29
 
21
30
  return {
@@ -29,7 +38,7 @@ export async function resolveActiveSourceWorkspace(config) {
29
38
  contentLabel: "React MDX chapter source",
30
39
  missingCode: "react-source.missing",
31
40
  emptyCode: "react-source.empty",
32
- missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in document/index.tsx `sources` before running export.",
41
+ missingMessage: "Registered React MDX sources do not exist yet; create the files or roots declared in press/index.tsx `sources` before running export.",
33
42
  emptyMessage: "Registered React MDX sources contain no `*.mdx` files; the document will export with zero source blocks.",
34
43
  };
35
44
  }
@@ -17,6 +17,20 @@ const PUBLIC_DEPLOY_ADAPTERS = new Set([
17
17
  "vercel",
18
18
  ]);
19
19
 
20
+ // A directory is an OpenPress workspace if it contains a
21
+ // press/index.tsx entry, or a package.json with an "openpress" field.
22
+ async function isWorkspaceRoot(dir) {
23
+ try {
24
+ await fs.access(path.join(dir, "press", "index.tsx"));
25
+ return true;
26
+ } catch {}
27
+ try {
28
+ const pkg = JSON.parse(await fs.readFile(path.join(dir, "package.json"), "utf8"));
29
+ if (pkg?.openpress && typeof pkg.openpress === "object") return true;
30
+ } catch {}
31
+ return false;
32
+ }
33
+
20
34
  export async function discoverWorkspace(startPath = ".") {
21
35
  let current = path.resolve(startPath);
22
36
  try {
@@ -26,15 +40,10 @@ export async function discoverWorkspace(startPath = ".") {
26
40
  current = path.dirname(current);
27
41
  }
28
42
  while (true) {
29
- const configPath = path.join(current, "openpress.config.mjs");
30
- try {
31
- await fs.access(configPath);
32
- return current;
33
- } catch {
34
- const parent = path.dirname(current);
35
- if (parent === current) throw new Error(`No OpenPress workspace found from ${startPath}`);
36
- current = parent;
37
- }
43
+ if (await isWorkspaceRoot(current)) return current;
44
+ const parent = path.dirname(current);
45
+ if (parent === current) throw new Error(`No OpenPress workspace found from ${startPath}`);
46
+ current = parent;
38
47
  }
39
48
  }
40
49
 
@@ -78,7 +87,7 @@ export async function validateWorkspace(root) {
78
87
 
79
88
  mark(sourceWorkspace.checkedName);
80
89
  if (!(typeof activeConfig.title === "string" && activeConfig.title.trim())) {
81
- add("warning", "config.title", "openpress.config.mjs `title` is empty; the workbench will show the default placeholder.", activeConfig.configPath);
90
+ add("warning", "press.title", "<Press title> is missing in press/index.tsx; the workbench will show the default placeholder.", activeConfig.configPath);
82
91
  }
83
92
  if (!(await sourceDirectoryExists(sourceWorkspace))) {
84
93
  add("warning", sourceWorkspace.missingCode, sourceWorkspace.missingMessage, sourceWorkspace.sourceDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/core",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "open-press core — runtime primitives, CLI, and render pipeline for AI-first fixed-layout documents.",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@
48
48
  "dependencies": {
49
49
  "@mdx-js/mdx": "^3.1.1",
50
50
  "@mdx-js/react": "^3.1.1",
51
+ "html-to-image": "^1.11.13",
51
52
  "js-yaml": "^4.1.1",
52
53
  "katex": "^0.16.47",
53
54
  "lucide-react": "^1.16.0",
@@ -80,11 +81,8 @@
80
81
  "test:e2e:reader": "playwright test --config playwright.reader.config.ts",
81
82
  "test:node": "node --test tests/*.test.mjs",
82
83
  "test:react": "vitest run",
83
- "openpress:validate": "node engine/cli.mjs validate .",
84
- "openpress:export": "node engine/cli.mjs export .",
84
+ "openpress:image": "node engine/cli.mjs image .",
85
85
  "openpress:pdf": "node engine/cli.mjs pdf .",
86
- "openpress:render": "node engine/cli.mjs render . --renderer react",
87
- "openpress:preview": "node engine/cli.mjs preview . --renderer react",
88
86
  "openpress:deploy": "node engine/cli.mjs deploy .",
89
87
  "openpress:deploy:dry-run": "node engine/cli.mjs deploy . --confirm --dry-run"
90
88
  }