@open-press/core 0.7.1 → 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 (144) 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/dev.mjs +2 -2
  5. package/engine/commands/image.mjs +29 -0
  6. package/engine/commands/skills-sync.mjs +71 -0
  7. package/engine/commands/typecheck.mjs +63 -1
  8. package/engine/commands/upgrade.mjs +3 -3
  9. package/engine/document-export.mjs +1 -1
  10. package/engine/output/chrome-pdf.mjs +110 -3
  11. package/engine/output/static-server.mjs +87 -9
  12. package/engine/react/comment-endpoint.mjs +13 -39
  13. package/engine/react/comment-marker.mjs +43 -19
  14. package/engine/react/document-entry.mjs +46 -28
  15. package/engine/react/document-export.mjs +328 -164
  16. package/engine/react/http-json.mjs +24 -0
  17. package/engine/react/mdx-compile.mjs +126 -3
  18. package/engine/react/measurement-css.mjs +114 -1
  19. package/engine/react/object-entities.mjs +204 -0
  20. package/engine/react/pagination/allocator.mjs +48 -3
  21. package/engine/react/pagination.mjs +1 -1
  22. package/engine/react/pipeline/allocate.mjs +41 -72
  23. package/engine/react/pipeline/frame-measurement.mjs +6 -0
  24. package/engine/react/press-tree-inspection.mjs +172 -0
  25. package/engine/react/project-asset-endpoint.mjs +6 -24
  26. package/engine/react/source-edit-endpoint.d.mts +10 -0
  27. package/engine/react/source-edit-endpoint.mjs +75 -0
  28. package/engine/react/sources/mdx-resolver.mjs +13 -15
  29. package/engine/react/style-discovery.mjs +23 -8
  30. package/engine/runtime/config.d.mts +8 -0
  31. package/engine/runtime/config.mjs +57 -60
  32. package/engine/runtime/file-utils.mjs +9 -1
  33. package/engine/runtime/file-walk.mjs +22 -0
  34. package/engine/runtime/inspection.mjs +1 -20
  35. package/engine/runtime/page-geometry.mjs +131 -0
  36. package/engine/runtime/path-utils.mjs +20 -0
  37. package/engine/runtime/source-text-tools.d.mts +102 -0
  38. package/engine/runtime/source-text-tools.mjs +551 -16
  39. package/engine/runtime/source-workspace.mjs +16 -34
  40. package/engine/runtime/validation.mjs +19 -10
  41. package/package.json +3 -5
  42. package/src/openpress/app/OpenPressApp.tsx +296 -0
  43. package/src/openpress/{renderer.tsx → app/OpenPressRuntime.tsx} +20 -9
  44. package/src/openpress/app/WorkspaceGalleryPage.tsx +219 -0
  45. package/src/openpress/app/index.ts +2 -0
  46. package/src/openpress/core/Frame.tsx +26 -15
  47. package/src/openpress/core/FrameContext.tsx +10 -3
  48. package/src/openpress/core/MdxArea.tsx +11 -12
  49. package/src/openpress/core/Press.tsx +25 -4
  50. package/src/openpress/core/Workspace.tsx +36 -0
  51. package/src/openpress/core/cn.ts +4 -0
  52. package/src/openpress/core/index.tsx +11 -3
  53. package/src/openpress/core/primitives.tsx +74 -6
  54. package/src/openpress/core/types.ts +94 -41
  55. package/src/openpress/core/useSource.ts +1 -1
  56. package/src/openpress/{anchorMap.ts → document-model/anchorMapModel.ts} +1 -1
  57. package/src/openpress/{indexes.ts → document-model/documentIndexes.ts} +1 -1
  58. package/src/openpress/{types.ts → document-model/documentTypes.ts} +51 -0
  59. package/src/openpress/document-model/index.ts +7 -0
  60. package/src/openpress/document-model/objectEntityModel.ts +55 -0
  61. package/src/openpress/{projectIdentity.ts → document-model/projectIdentityModel.ts} +1 -1
  62. package/src/openpress/{reactDocumentMetadata.ts → document-model/reactDocumentMetadataModel.ts} +1 -1
  63. package/src/openpress/document-model/workspaceManifestModel.ts +57 -0
  64. package/src/openpress/manuscript/index.tsx +49 -7
  65. package/src/openpress/mdx/index.ts +15 -7
  66. package/src/openpress/reader/PageThumbnailsPanel.tsx +168 -0
  67. package/src/openpress/{publicPage.tsx → reader/PublicReaderPage.tsx} +31 -51
  68. package/src/openpress/{workbenchPanels.tsx → reader/ReaderNavigationPanel.tsx} +6 -5
  69. package/src/openpress/reader/index.ts +11 -0
  70. package/src/openpress/reader/pageViewportScaleModel.ts +73 -0
  71. package/src/openpress/reader/readerTypes.ts +4 -0
  72. package/src/openpress/reader/usePageViewportScale.ts +119 -0
  73. package/src/openpress/reader/usePanelState.ts +56 -0
  74. package/src/openpress/reader/useReaderHashSync.ts +61 -0
  75. package/src/openpress/reader/useReaderKeyboardNav.ts +48 -0
  76. package/src/openpress/reader/useReaderRuntime.ts +146 -0
  77. package/src/openpress/reader/useReaderScrollAnchor.ts +64 -0
  78. package/src/openpress/shared/Panel.tsx +77 -0
  79. package/src/openpress/shared/index.ts +4 -0
  80. package/src/openpress/shared/numberUtils.ts +3 -0
  81. package/src/openpress/{runtimeMode.ts → shared/runtimeMode.ts} +0 -11
  82. package/src/openpress/workbench/Workbench.tsx +506 -0
  83. package/src/openpress/workbench/actions/DeploymentControl.tsx +157 -0
  84. package/src/openpress/workbench/actions/ExportImageControl.tsx +96 -0
  85. package/src/openpress/workbench/actions/PageZoomControl.tsx +182 -0
  86. package/src/openpress/workbench/actions/SearchControl.tsx +345 -0
  87. package/src/openpress/workbench/actions/deploymentStatusModel.ts +112 -0
  88. package/src/openpress/workbench/actions/index.ts +6 -0
  89. package/src/openpress/workbench/actions/useDeploymentWorkbench.ts +136 -0
  90. package/src/openpress/workbench/dialog/WorkbenchDialog.tsx +72 -0
  91. package/src/openpress/workbench/dialog/index.ts +1 -0
  92. package/src/openpress/workbench/document/components/DocumentPanel.tsx +127 -0
  93. package/src/openpress/workbench/document/components/InlineSourceEditorLayer.tsx +207 -0
  94. package/src/openpress/workbench/document/components/ReaderStage.tsx +9 -0
  95. package/src/openpress/workbench/document/hooks/useDocumentWorkbenchModel.ts +34 -0
  96. package/src/openpress/workbench/document/hooks/useInlineDocumentEditor.ts +525 -0
  97. package/src/openpress/workbench/document/index.ts +10 -0
  98. package/src/openpress/workbench/index.ts +2 -0
  99. package/src/openpress/workbench/inspector/InlineInspectorLayer.tsx +459 -0
  100. package/src/openpress/workbench/inspector/index.ts +5 -0
  101. package/src/openpress/workbench/inspector/inlineCommentModel.ts +125 -0
  102. package/src/openpress/workbench/inspector/inspectorGeometryModel.ts +160 -0
  103. package/src/openpress/workbench/inspector/inspectorModel.ts +408 -0
  104. package/src/openpress/workbench/inspector/useInspectorComments.ts +254 -0
  105. package/src/openpress/workbench/mentions/MentionSuggestionList.tsx +41 -0
  106. package/src/openpress/workbench/mentions/index.ts +2 -0
  107. package/src/openpress/{composerMentions.ts → workbench/mentions/useComposerMentions.ts} +1 -4
  108. package/src/openpress/workbench/panels/Panel.tsx +1 -0
  109. package/src/openpress/workbench/panels/PendingCommentsPanel.tsx +80 -0
  110. package/src/openpress/workbench/panels/WorkbenchControlPanel.tsx +29 -0
  111. package/src/openpress/workbench/panels/index.ts +3 -0
  112. package/src/openpress/workbench/project/ProjectEntryPanel.tsx +525 -0
  113. package/src/openpress/workbench/project/ProjectPreviewDialog.tsx +35 -0
  114. package/src/openpress/workbench/project/index.ts +2 -0
  115. package/src/openpress/workbench/project/projectPreviewTypes.ts +11 -0
  116. package/src/openpress/workbench/shell/WorkbenchShell.tsx +167 -0
  117. package/src/openpress/workbench/shell/index.ts +1 -0
  118. package/src/openpress/workbench/workbenchFormatters.ts +120 -0
  119. package/src/openpress/workbench/workbenchTypes.ts +35 -0
  120. package/src/styles/openpress/print-route.css +0 -2
  121. package/src/styles/openpress/{project-workspace.css → project-preview-panel.css} +13 -407
  122. package/src/styles/openpress/public-viewer.css +25 -320
  123. package/src/styles/openpress/reader-runtime.css +252 -55
  124. package/src/styles/openpress/responsive.css +145 -270
  125. package/src/styles/openpress/workbench-panels.css +327 -178
  126. package/src/styles/openpress/workbench.css +986 -451
  127. package/src/styles/openpress/workspace-gallery.css +300 -0
  128. package/src/styles/openpress.css +2 -1
  129. package/tsconfig.json +1 -1
  130. package/vite.config.ts +50 -0
  131. package/engine/commands/init.mjs +0 -24
  132. package/engine/init.mjs +0 -90
  133. package/src/openpress/App.tsx +0 -127
  134. package/src/openpress/inspector.ts +0 -282
  135. package/src/openpress/projectWorkspace.tsx +0 -919
  136. package/src/openpress/readerRuntime.ts +0 -230
  137. package/src/openpress/workbench.tsx +0 -1265
  138. package/src/openpress/workbenchTypes.ts +0 -4
  139. /package/src/openpress/{readerPageRegistry.ts → reader/readerPageRegistry.ts} +0 -0
  140. /package/src/openpress/{pageRoute.ts → reader/readerPageRoute.ts} +0 -0
  141. /package/src/openpress/{readerScroll.ts → reader/readerScroll.ts} +0 -0
  142. /package/src/openpress/{readerState.ts → reader/readerStateModel.ts} +0 -0
  143. /package/src/openpress/{frameScheduler.ts → shared/frameScheduler.ts} +0 -0
  144. /package/src/openpress/{projectSources.ts → workbench/project/projectSourceModel.ts} +0 -0
package/README.md CHANGED
@@ -5,10 +5,10 @@ Framework runtime, CLI engine, and Press Tree primitives for [open-press](https:
5
5
  Most users do **not** install this package directly. Instead, scaffold a workspace with the CLI:
6
6
 
7
7
  ```bash
8
- npx @open-press/cli init my-doc --pack editorial-monograph
8
+ npx @open-press/cli init my-doc
9
9
  ```
10
10
 
11
- The scaffolded workspace contains a snapshot of this package.
11
+ The scaffolded workspace contains a snapshot of this package. Starter files are supplied by skills, not by `@open-press/core`.
12
12
 
13
13
  ## Direct use
14
14
 
@@ -31,7 +31,10 @@ import { mdxSource } from "@open-press/core/mdx";
31
31
  import { Sections, Toc } from "@open-press/core/manuscript";
32
32
  ```
33
33
 
34
- `document/index.tsx` default-exports a `<Press>` tree. `Frame` marks fixed-layout pages, `MdxArea` receives measured MDX blocks, and `mdxSource()` declares which MDX files participate in the render pipeline.
34
+ `press/index.tsx` or transitional `document/index.tsx` default-exports a `<Workspace>/<Press>` tree. `Frame` marks fixed-layout pages, `MdxArea` receives measured MDX blocks, and `mdxSource()` declares which MDX files participate in the render pipeline.
35
+
36
+ For the maintenance contract around Press Tree, page geometry presets, and the
37
+ allocation pipeline, see [`docs/press-tree.md`](https://github.com/quan0715/open-press/blob/main/docs/press-tree.md).
35
38
 
36
39
  The CLI bin (`open-press`) supports dev / build / preview / validate / pdf / deploy / export commands. It requires a workspace with `openpress.config.mjs` and the surrounding framework files (which the scaffolder installs).
37
40
 
package/engine/cli.mjs CHANGED
@@ -4,25 +4,25 @@ import * as deployCmd from "./commands/deploy.mjs";
4
4
  import * as devCmd from "./commands/dev.mjs";
5
5
  import * as doctorCmd from "./commands/doctor.mjs";
6
6
  import * as exportCmd from "./commands/export.mjs";
7
- import * as initCmd from "./commands/init.mjs";
8
7
  import * as inspectCmd from "./commands/inspect.mjs";
8
+ import * as imageCmd from "./commands/image.mjs";
9
9
  import * as pdfCmd from "./commands/pdf.mjs";
10
10
  import * as previewCmd from "./commands/preview.mjs";
11
11
  import * as replaceCmd from "./commands/replace.mjs";
12
12
  import * as renderCmd from "./commands/render.mjs";
13
13
  import * as searchCmd from "./commands/search.mjs";
14
+ import * as skillsSyncCmd from "./commands/skills-sync.mjs";
14
15
  import * as typecheckCmd from "./commands/typecheck.mjs";
15
16
  import * as upgradeCmd from "./commands/upgrade.mjs";
16
17
  import * as validateCmd from "./commands/validate.mjs";
17
18
  import { parseOptions } from "./commands/_shared.mjs";
18
19
  import { loadConfig } from "./runtime/config.mjs";
19
- import { listStylePackSkills } from "./init.mjs";
20
20
  import { discoverWorkspace } from "./runtime/validation.mjs";
21
21
 
22
22
  const COMMANDS = {
23
- init: initCmd,
24
23
  validate: validateCmd,
25
24
  inspect: inspectCmd,
25
+ image: imageCmd,
26
26
  search: searchCmd,
27
27
  replace: replaceCmd,
28
28
  export: exportCmd,
@@ -34,6 +34,8 @@ const COMMANDS = {
34
34
  deploy: deployCmd,
35
35
  doctor: doctorCmd,
36
36
  upgrade: upgradeCmd,
37
+ migrate: upgradeCmd,
38
+ "skills:sync": skillsSyncCmd,
37
39
  };
38
40
 
39
41
  const args = process.argv.slice(2);
@@ -71,12 +73,9 @@ async function main(commandName, argv) {
71
73
  }
72
74
 
73
75
  async function printHelp() {
74
- const packs = await listStylePackSkills();
75
- const skillList = packs.length ? packs.join(" | ") : "(none installed)";
76
76
  console.log(`Usage: node engine/cli.mjs <command> [path] [options]
77
77
 
78
78
  Commands:
79
- init <target> [--skill <name>] [--force]
80
79
  validate
81
80
  inspect [--json] [--no-build] [--dry-run]
82
81
  search [path] <query> [--json] [--scope content|all]
@@ -86,11 +85,12 @@ Commands:
86
85
  preview --renderer react [--host 127.0.0.1] [--port 5173] [--no-build] [--dry-run]
87
86
  dev --renderer react [--host 127.0.0.1] [--port 5173] [--no-build] [--dry-run]
88
87
  typecheck
88
+ image [--output <outputDir>] [--no-build] [--dry-run]
89
89
  pdf [--output <outputDir>/<pdf.filename>] [--no-build] [--dry-run]
90
90
  deploy --confirm [--dry-run]
91
91
  doctor [--json] [--no-cache] # version + skill staleness check
92
92
  upgrade [--dry-run] [--no-deps] [--no-skills] [--json] # apply updates; agent-driven
93
-
94
- Style packs available for \`init --skill\`: ${skillList}
93
+ migrate [--dry-run] [--no-deps] [--no-skills] [--json] # alias for upgrade; reads migration notes
94
+ skills:sync [--source <owner/repo>] [--dry-run] # refresh installed agent skills
95
95
  `);
96
96
  }
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
5
+ import { captureUrlPagesToPng, printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
6
6
  import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
7
7
  import { exportDocument } from "../document-export.mjs";
8
8
  import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
@@ -23,6 +23,9 @@ export function parseOptions(argv) {
23
23
  else if (value === "--force") options.force = true;
24
24
  else if (value === "--confirm") options.confirm = true;
25
25
  else if (value === "--json") options.json = true;
26
+ else if (value === "--no-cache") options.noCache = true;
27
+ else if (value === "--no-deps") options.noDeps = true;
28
+ else if (value === "--no-skills") options.noSkills = true;
26
29
  else if (value === "--no-build") options.noBuild = true;
27
30
  else if (value === "--apply") options.apply = true;
28
31
  else if (value === "--include-code") options.includeCode = true;
@@ -38,20 +41,6 @@ export function parseOptions(argv) {
38
41
  return options;
39
42
  }
40
43
 
41
- export function parseInitOptions(argv) {
42
- const options = { force: false };
43
- const positional = [];
44
- for (let i = 0; i < argv.length; i += 1) {
45
- const value = argv[i];
46
- if (value === "--skill") options.skill = argv[++i];
47
- else if (value === "--force") options.force = true;
48
- else if (value.startsWith("--")) throw new Error(`Unknown option: ${value}`);
49
- else positional.push(value);
50
- }
51
- options.target = positional[0];
52
- return options;
53
- }
54
-
55
44
  export function formatDisplayPath(absolutePath) {
56
45
  const relative = path.relative(process.cwd(), absolutePath);
57
46
  if (!relative || relative.startsWith("..")) return absolutePath;
@@ -118,6 +107,39 @@ export async function buildReactPdf({
118
107
  return { pdfPath: outPath };
119
108
  }
120
109
 
110
+ export async function buildReactImages({
111
+ root,
112
+ config,
113
+ outDir,
114
+ host = "127.0.0.1",
115
+ port = "5186",
116
+ noBuild = false,
117
+ recurse,
118
+ }) {
119
+ config ??= await loadConfig(root);
120
+ outDir ??= path.join(config.paths.outputDir, "images");
121
+ const renderCode = await buildReactStatic({ root, noBuild, recurse });
122
+ if (renderCode !== 0) throw new Error(`React render failed with exit code ${renderCode}`);
123
+ await fs.mkdir(outDir, { recursive: true });
124
+
125
+ const server = await startStaticServer(root, config, host, port);
126
+ try {
127
+ const result = await captureUrlPagesToPng({
128
+ root,
129
+ url: `http://${host}:${port}/?print=1`,
130
+ outDir,
131
+ waitForReady: waitForPrintReady,
132
+ debuggingPortBase: 9700,
133
+ debuggingPortRange: 600,
134
+ profilePrefix: "chrome-image",
135
+ });
136
+ console.log(`${result.files.length} OpenPress pages exported to PNG`);
137
+ return { outDir, files: result.files, pageCount: result.pageCount };
138
+ } finally {
139
+ await stopChildProcess(server);
140
+ }
141
+ }
142
+
121
143
  export function startStaticServer(root, config, host, port) {
122
144
  return new Promise((resolve, reject) => {
123
145
  const child = spawn("node", [STATIC_SERVER, config.outputDir, "--host", host, "--port", port, "--workspace", "."], {
@@ -16,7 +16,7 @@ export async function run({ root, options }) {
16
16
  if (!options.noBuild) {
17
17
  console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} export .`);
18
18
  }
19
- console.log(`Command: npx vite --config vite.config.ts --host ${host} --port ${port}`);
19
+ console.log(`Command: npx vite --force --config vite.config.ts --host ${host} --port ${port}`);
20
20
  return 0;
21
21
  }
22
22
  if (!options.noBuild) {
@@ -27,7 +27,7 @@ export async function run({ root, options }) {
27
27
  await printDoctorNoticeIfStale(root);
28
28
 
29
29
  console.log(`OpenPress dev: ${url}`);
30
- return runCommand("npx", ["vite", "--config", "vite.config.ts", "--host", host, "--port", port], root);
30
+ return runCommand("npx", ["vite", "--force", "--config", "vite.config.ts", "--host", host, "--port", port], root);
31
31
  }
32
32
 
33
33
  async function printDoctorNoticeIfStale(root) {
@@ -0,0 +1,29 @@
1
+ import path from "node:path";
2
+ import { CLI_ENTRY, STATIC_SERVER, buildReactImages, formatNodeScriptCommand } from "./_shared.mjs";
3
+
4
+ export async function run({ root, config, options, recurse }) {
5
+ const outputDir = options.output ? path.resolve(root, options.output) : path.join(config.paths.outputDir, "images");
6
+ const host = options.host ?? "127.0.0.1";
7
+ const port = options.port ?? "5186";
8
+
9
+ if (options.dryRun) {
10
+ console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
11
+ console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
12
+ console.log(`Chrome image export URL: http://${host}:${port}/?print=1`);
13
+ console.log(`Output: ${path.relative(root, path.join(outputDir, "page-001.png"))}`);
14
+ return 0;
15
+ }
16
+
17
+ const result = await buildReactImages({
18
+ root,
19
+ config,
20
+ outDir: outputDir,
21
+ host,
22
+ port,
23
+ noBuild: options.noBuild,
24
+ recurse,
25
+ });
26
+
27
+ console.log(`OpenPress images: ${path.relative(root, result.outDir)} (${result.files.length} pages)`);
28
+ return 0;
29
+ }
@@ -0,0 +1,71 @@
1
+ import path from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { runCommand } from "./_shared.mjs";
5
+
6
+ const DEFAULT_SOURCE = "quan0715/open-press";
7
+
8
+ // Refresh installed agent skills against the workspace's lock file.
9
+ // Behavior:
10
+ // - If skills-lock.json exists, run `npx skills upgrade` (refreshes all
11
+ // currently-installed sources to their latest published versions).
12
+ // - If skills-lock.json is missing, install the OpenPress framework
13
+ // skill bundle (and any user-supplied --source) as a first-time setup.
14
+ // - If a --source flag is passed, also add that source on top of any
15
+ // existing installations.
16
+ //
17
+ // Always exits 0 unless the underlying `skills` tool fails.
18
+ export async function run({ root, options }) {
19
+ const lockPath = path.join(root, "skills-lock.json");
20
+ const lockExists = existsSync(lockPath);
21
+ const extraSource = options?.source;
22
+
23
+ if (options?.dryRun) {
24
+ if (lockExists) {
25
+ console.log("Command: npx -y skills@latest upgrade");
26
+ } else {
27
+ console.log(`Command: npx -y skills@latest add ${DEFAULT_SOURCE}`);
28
+ }
29
+ if (extraSource) {
30
+ console.log(`Command: npx -y skills@latest add ${extraSource}`);
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ if (lockExists) {
36
+ const sources = await readLockSources(lockPath);
37
+ if (sources.length === 0) {
38
+ console.log("skills-lock.json has no sources; installing framework default…");
39
+ const code = await runCommand("npx", ["-y", "skills@latest", "add", DEFAULT_SOURCE], root);
40
+ if (code !== 0) return code;
41
+ } else {
42
+ console.log(`Refreshing ${sources.length} installed source(s)…`);
43
+ for (const src of sources) console.log(` ${src}`);
44
+ const code = await runCommand("npx", ["-y", "skills@latest", "upgrade"], root);
45
+ if (code !== 0) return code;
46
+ }
47
+ } else {
48
+ console.log(`No skills-lock.json; installing framework default: ${DEFAULT_SOURCE}`);
49
+ const code = await runCommand("npx", ["-y", "skills@latest", "add", DEFAULT_SOURCE], root);
50
+ if (code !== 0) return code;
51
+ }
52
+
53
+ if (extraSource) {
54
+ console.log(`Adding extra source: ${extraSource}`);
55
+ const code = await runCommand("npx", ["-y", "skills@latest", "add", extraSource], root);
56
+ if (code !== 0) return code;
57
+ }
58
+
59
+ console.log("✓ Skills synced");
60
+ return 0;
61
+ }
62
+
63
+ async function readLockSources(lockPath) {
64
+ try {
65
+ const lock = JSON.parse(await readFile(lockPath, "utf8"));
66
+ const sources = Array.isArray(lock?.sources) ? lock.sources : [];
67
+ return sources.map((s) => s?.source).filter((s) => typeof s === "string");
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
@@ -1,5 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
1
4
  import { runCommand } from "./_shared.mjs";
2
5
 
6
+ // Run typecheck via the locally installed typescript. The previous
7
+ // implementation used `npx tsc`; npm 11 + Node 24 (our CI / release
8
+ // pin) changed npx's bin lookup so it no longer walks pnpm's nested
9
+ // `.bin/` symlink farm and falls back to fetching the legacy
10
+ // `tsc@2.0.4` shim, which crashes.
11
+ //
12
+ // Resolution order:
13
+ // 1. `node <resolved tsc>` via require.resolve(typescript/package.json)
14
+ // — works with npm-hoisted layouts and most pnpm installs.
15
+ // 2. Walk up node_modules/.bin/tsc — covers downstream npm/yarn.
16
+ // 3. Fall back to `pnpm exec tsc` — pnpm knows its own symlink farm
17
+ // even when bare require.resolve doesn't, which is what CI hits.
3
18
  export async function run({ root }) {
4
- return runCommand("npx", ["tsc", "--noEmit", "-p", "tsconfig.json"], root);
19
+ const absoluteRoot = path.resolve(root);
20
+
21
+ const tscBin = resolveTscBin(absoluteRoot);
22
+ if (tscBin) {
23
+ return runCommand("node", [tscBin, "--noEmit", "-p", "tsconfig.json"], absoluteRoot);
24
+ }
25
+
26
+ if (hasCommand("pnpm")) {
27
+ return runCommand("pnpm", ["exec", "tsc", "--noEmit", "-p", "tsconfig.json"], absoluteRoot);
28
+ }
29
+
30
+ console.error("[openpress] typescript is not installed in this workspace.");
31
+ console.error("Add it with: npm install --save-dev typescript");
32
+ return 1;
33
+ }
34
+
35
+ function resolveTscBin(absoluteRoot) {
36
+ try {
37
+ const require = createRequire(path.join(absoluteRoot, "package.json"));
38
+ const pkgPath = require.resolve("typescript/package.json");
39
+ return path.join(path.dirname(pkgPath), "bin", "tsc");
40
+ } catch {
41
+ // fall through to .bin probe
42
+ }
43
+
44
+ let dir = absoluteRoot;
45
+ while (true) {
46
+ const candidate = path.join(dir, "node_modules", ".bin", "tsc");
47
+ if (fs.existsSync(candidate)) return candidate;
48
+ const parent = path.dirname(dir);
49
+ if (parent === dir) return null;
50
+ dir = parent;
51
+ }
52
+ }
53
+
54
+ function hasCommand(name) {
55
+ const PATH = process.env.PATH ?? "";
56
+ const sep = process.platform === "win32" ? ";" : ":";
57
+ const exts = process.platform === "win32"
58
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
59
+ : [""];
60
+ for (const dir of PATH.split(sep)) {
61
+ if (!dir) continue;
62
+ for (const ext of exts) {
63
+ if (fs.existsSync(path.join(dir, name + ext))) return true;
64
+ }
65
+ }
66
+ return false;
5
67
  }
@@ -46,7 +46,7 @@ export async function run({ root, options }) {
46
46
  if (!json) {
47
47
  process.stdout.write("dry run — nothing changed. The agent should:\n");
48
48
  process.stdout.write(" 1. read each docs/migrations/<version>.md for document-level changes\n");
49
- process.stdout.write(" 2. apply edits to document/ where needed\n");
49
+ process.stdout.write(" 2. apply edits to press/ where needed\n");
50
50
  process.stdout.write(" 3. re-run: npx open-press upgrade (without --dry-run)\n");
51
51
  } else {
52
52
  process.stdout.write(JSON.stringify({ status: "dry-run", before }, null, 2) + "\n");
@@ -98,11 +98,11 @@ export async function run({ root, options }) {
98
98
  }
99
99
  }
100
100
  process.stdout.write(
101
- "\nAgent: open each file, identify document-level changes, grep document/ for affected patterns, propose edits before applying.\n",
101
+ "\nAgent: open each file, identify document-level changes, grep press/ for affected patterns, propose edits before applying.\n",
102
102
  );
103
103
  }
104
104
 
105
- process.stdout.write("\nVerify with:\n npm run openpress:validate\n npm run openpress:render\n\n");
105
+ process.stdout.write("\nVerify with:\n npm run build\n\n");
106
106
  return 0;
107
107
  }
108
108
 
@@ -10,6 +10,6 @@ export async function exportDocument(root = ROOT) {
10
10
  if (reactResult) return reactResult;
11
11
 
12
12
  throw new Error(
13
- "React/MDX document entry not found. Expected document/index.tsx with a Press default export before exporting.",
13
+ "React/MDX document entry not found. Expected press/index.tsx with a Press default export before exporting.",
14
14
  );
15
15
  }
@@ -91,6 +91,13 @@ const DEFAULT_PRINT_OPTIONS = {
91
91
  marginLeft: 0,
92
92
  };
93
93
 
94
+ export const DEFAULT_PRINT_VIEWPORT = Object.freeze({
95
+ width: 1200,
96
+ height: 1698,
97
+ deviceScaleFactor: 1,
98
+ mobile: false,
99
+ });
100
+
94
101
  export async function printUrlToPdf({
95
102
  root,
96
103
  url,
@@ -98,6 +105,7 @@ export async function printUrlToPdf({
98
105
  chrome,
99
106
  waitForReady = waitForPrintReady,
100
107
  printOptions = {},
108
+ viewport = DEFAULT_PRINT_VIEWPORT,
101
109
  debuggingPortBase = 9600,
102
110
  debuggingPortRange = 300,
103
111
  profilePrefix = "chrome-pdf",
@@ -126,9 +134,7 @@ export async function printUrlToPdf({
126
134
  const tab = await waitForChromeTab(debuggingPort);
127
135
  const client = await connectChromeDevTools(tab.webSocketDebuggerUrl);
128
136
  try {
129
- await client.send("Page.enable");
130
- await client.send("Runtime.enable");
131
- await client.send("Emulation.setEmulatedMedia", { media: "print" });
137
+ await preparePdfPage(client, { viewport });
132
138
  await client.send("Page.navigate", { url });
133
139
  const readyResult = await waitForReady(client);
134
140
  const result = await client.send("Page.printToPDF", {
@@ -146,6 +152,87 @@ export async function printUrlToPdf({
146
152
  }
147
153
  }
148
154
 
155
+ export async function captureUrlPagesToPng({
156
+ root,
157
+ url,
158
+ outDir,
159
+ chrome,
160
+ waitForReady = waitForPrintReady,
161
+ viewport = DEFAULT_PRINT_VIEWPORT,
162
+ debuggingPortBase = 9700,
163
+ debuggingPortRange = 300,
164
+ profilePrefix = "chrome-image",
165
+ }) {
166
+ chrome ??= resolveChromePath();
167
+ await fs.mkdir(outDir, { recursive: true });
168
+
169
+ const debuggingPort = String(debuggingPortBase + Math.floor(Math.random() * debuggingPortRange));
170
+ const profileDir = path.join(root, ".openpress", "tmp", `${profilePrefix}-${process.pid}-${Date.now()}`);
171
+ await fs.mkdir(profileDir, { recursive: true });
172
+
173
+ const child = spawn(
174
+ chrome,
175
+ [
176
+ "--headless=new",
177
+ "--disable-gpu",
178
+ "--no-sandbox",
179
+ `--remote-debugging-port=${debuggingPort}`,
180
+ `--user-data-dir=${profileDir}`,
181
+ "about:blank",
182
+ ],
183
+ { cwd: root, stdio: ["ignore", "pipe", "pipe"] },
184
+ );
185
+
186
+ try {
187
+ const tab = await waitForChromeTab(debuggingPort);
188
+ const client = await connectChromeDevTools(tab.webSocketDebuggerUrl);
189
+ try {
190
+ await preparePdfPage(client, { viewport });
191
+ await client.send("Page.navigate", { url });
192
+ const pageCount = await waitForReady(client);
193
+ const rects = await getPrintPageRects(client);
194
+ if (rects.length === 0) throw new Error("No OpenPress pages found for image export.");
195
+
196
+ const padWidth = Math.max(3, String(rects.length).length);
197
+ const files = [];
198
+ for (const [index, rect] of rects.entries()) {
199
+ const filename = `page-${String(index + 1).padStart(padWidth, "0")}.png`;
200
+ const filePath = path.join(outDir, filename);
201
+ const result = await client.send("Page.captureScreenshot", {
202
+ format: "png",
203
+ fromSurface: true,
204
+ captureBeyondViewport: true,
205
+ clip: {
206
+ x: Math.max(0, rect.x),
207
+ y: Math.max(0, rect.y),
208
+ width: rect.width,
209
+ height: rect.height,
210
+ scale: 1,
211
+ },
212
+ });
213
+ await fs.writeFile(filePath, Buffer.from(String(result.data ?? ""), "base64"));
214
+ files.push(filePath);
215
+ }
216
+
217
+ return { pageCount, files };
218
+ } finally {
219
+ client.close();
220
+ }
221
+ } finally {
222
+ await stopChildProcess(child);
223
+ await cleanupChromeProfile(profileDir);
224
+ }
225
+ }
226
+
227
+ export async function preparePdfPage(client, { viewport = DEFAULT_PRINT_VIEWPORT } = {}) {
228
+ await client.send("Page.enable");
229
+ await client.send("Runtime.enable");
230
+ if (viewport) {
231
+ await client.send("Emulation.setDeviceMetricsOverride", viewport);
232
+ }
233
+ await client.send("Emulation.setEmulatedMedia", { media: "print" });
234
+ }
235
+
149
236
  export async function evaluateUrlWithChrome({
150
237
  root,
151
238
  url,
@@ -250,6 +337,26 @@ export async function waitForPrintReady(client) {
250
337
  throw new Error("Timed out waiting for OpenPress pagination before PDF export.");
251
338
  }
252
339
 
340
+ async function getPrintPageRects(client) {
341
+ const result = await client.send("Runtime.evaluate", {
342
+ returnByValue: true,
343
+ expression: `(() => {
344
+ return Array.from(document.querySelectorAll('.openpress-public-page > .openpress-html-page')).map((page, index) => {
345
+ const target = page.querySelector('.openpress-html-page__html') || page;
346
+ const rect = target.getBoundingClientRect();
347
+ return {
348
+ index,
349
+ x: rect.left + window.scrollX,
350
+ y: rect.top + window.scrollY,
351
+ width: rect.width,
352
+ height: rect.height,
353
+ };
354
+ }).filter((rect) => rect.width > 0 && rect.height > 0);
355
+ })()`,
356
+ });
357
+ return Array.isArray(result.result?.value) ? result.result.value : [];
358
+ }
359
+
253
360
  export async function stopChildProcess(child) {
254
361
  if (child.exitCode !== null || child.signalCode !== null) return;
255
362
  child.kill();