@open-press/cli 0.6.0 → 0.7.1

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 (97) hide show
  1. package/package.json +1 -1
  2. package/template/core/AGENTS.md +126 -0
  3. package/template/core/CHANGELOG.md +87 -1
  4. package/template/core/README.md +9 -5
  5. package/template/core/engine/cli.mjs +2 -5
  6. package/template/core/engine/commands/_shared.mjs +4 -4
  7. package/template/core/engine/commands/deploy.mjs +1 -1
  8. package/template/core/engine/commands/inspect.mjs +3 -3
  9. package/template/core/engine/commands/replace.mjs +1 -1
  10. package/template/core/engine/commands/search.mjs +1 -1
  11. package/template/core/engine/commands/upgrade.mjs +47 -5
  12. package/template/core/engine/commands/validate.mjs +2 -2
  13. package/template/core/engine/document-export.mjs +1 -1
  14. package/template/core/engine/{chrome-pdf.mjs → output/chrome-pdf.mjs} +1 -2
  15. package/template/core/engine/{deploy-sync.mjs → output/deploy-sync.mjs} +2 -2
  16. package/template/core/engine/{fonts.mjs → output/fonts.mjs} +1 -1
  17. package/template/core/engine/{public-assets.mjs → output/public-assets.mjs} +2 -2
  18. package/template/core/engine/{static-server.mjs → output/static-server.mjs} +2 -2
  19. package/template/core/engine/react/caption-numbering.mjs +73 -0
  20. package/template/core/engine/react/comment-marker.mjs +54 -10
  21. package/template/core/engine/react/document-entry.mjs +124 -64
  22. package/template/core/engine/react/document-export.mjs +266 -310
  23. package/template/core/engine/react/mdx-compile.mjs +214 -3
  24. package/template/core/engine/react/measurement-css.mjs +3 -3
  25. package/template/core/engine/react/pagination/allocator.mjs +122 -0
  26. package/template/core/engine/react/pagination/regions.mjs +81 -0
  27. package/template/core/engine/react/pagination.mjs +9 -121
  28. package/template/core/engine/react/pipeline/allocate.mjs +248 -0
  29. package/template/core/engine/react/pipeline/final-render.mjs +94 -0
  30. package/template/core/engine/react/pipeline/frame-measurement.mjs +300 -0
  31. package/template/core/engine/react/pipeline/press-tree.mjs +135 -0
  32. package/template/core/engine/react/project-asset-endpoint.mjs +2 -2
  33. package/template/core/engine/react/{chapter-css.mjs → section-css.mjs} +12 -9
  34. package/template/core/engine/react/sources/heading-numbering.mjs +132 -0
  35. package/template/core/engine/react/sources/mdx-resolver.mjs +441 -0
  36. package/template/core/engine/react/{workspace-discovery.mjs → style-discovery.mjs} +29 -40
  37. package/template/core/engine/{config.mjs → runtime/config.mjs} +15 -0
  38. package/template/core/engine/{file-utils.mjs → runtime/file-utils.mjs} +1 -1
  39. package/template/core/engine/{inspection.mjs → runtime/inspection.mjs} +3 -4
  40. package/template/core/engine/{source-text-tools.mjs → runtime/source-text-tools.mjs} +24 -7
  41. package/template/core/engine/runtime/source-workspace.mjs +186 -0
  42. package/template/core/engine/{validation.mjs → runtime/validation.mjs} +19 -17
  43. package/template/core/package.json +5 -2
  44. package/template/core/src/openpress/anchorMap.ts +27 -0
  45. package/template/core/src/openpress/core/Frame.tsx +80 -0
  46. package/template/core/src/openpress/core/FrameContext.tsx +19 -0
  47. package/template/core/src/openpress/core/MdxArea.tsx +35 -0
  48. package/template/core/src/openpress/core/Press.tsx +34 -0
  49. package/template/core/src/openpress/core/index.tsx +34 -15
  50. package/template/core/src/openpress/core/primitives.tsx +23 -0
  51. package/template/core/src/openpress/core/types.ts +131 -19
  52. package/template/core/src/openpress/core/useSource.ts +28 -0
  53. package/template/core/src/openpress/manuscript/index.tsx +196 -0
  54. package/template/core/src/openpress/mdx/index.ts +88 -0
  55. package/template/core/src/openpress/numbering/index.ts +294 -0
  56. package/template/core/src/openpress/publicPage.tsx +4 -186
  57. package/template/core/src/openpress/reactDocumentMetadata.ts +2 -16
  58. package/template/core/src/openpress/types.ts +0 -16
  59. package/template/core/src/openpress/workbench.tsx +2 -36
  60. package/template/core/src/styles/openpress/responsive.css +0 -14
  61. package/template/core/tsconfig.json +4 -1
  62. package/template/core/vite.config.ts +10 -3
  63. package/template/packs/academic-paper/document/chapters/01-introduction/content/01-introduction.mdx +26 -12
  64. package/template/packs/academic-paper/document/chapters/02-methods/content/01-methods.mdx +37 -17
  65. package/template/packs/academic-paper/document/chapters/03-results-and-discussion/content/01-results.mdx +34 -16
  66. package/template/packs/academic-paper/document/chapters/04-acknowledgment/content/01-acknowledgment.mdx +22 -8
  67. package/template/packs/academic-paper/document/chapters/05-references/content/01-references.mdx +20 -15
  68. package/template/packs/academic-paper/document/components/Page.tsx +48 -15
  69. package/template/packs/academic-paper/document/design.md +2 -2
  70. package/template/packs/academic-paper/document/index.tsx +96 -80
  71. package/template/packs/academic-paper/document/media/figure-placeholder.svg +9 -0
  72. package/template/packs/academic-paper/document/theme/base/page-contract.css +30 -13
  73. package/template/packs/academic-paper/document/theme/base/typography.css +30 -33
  74. package/template/packs/academic-paper/document/theme/page-surfaces/cover.css +74 -47
  75. package/template/packs/academic-paper/document/theme/page-surfaces/toc.css +19 -9
  76. package/template/packs/claude-document/document/components/Page.tsx +24 -14
  77. package/template/packs/claude-document/document/design.md +2 -2
  78. package/template/packs/claude-document/document/index.tsx +67 -62
  79. package/template/packs/claude-document/document/theme/page-surfaces/toc.css +19 -7
  80. package/template/packs/editorial-monograph/document/components/Page.tsx +24 -14
  81. package/template/packs/editorial-monograph/document/design.md +2 -2
  82. package/template/packs/editorial-monograph/document/index.tsx +71 -47
  83. package/template/packs/editorial-monograph/document/theme/page-surfaces/toc.css +19 -9
  84. package/template/core/engine/commands/migrate-to-react.mjs +0 -27
  85. package/template/core/engine/page-renderer.mjs +0 -217
  86. package/template/core/engine/react/migrate-to-react.mjs +0 -355
  87. package/template/core/engine/source-workspace.mjs +0 -76
  88. package/template/core/src/openpress/core/basePages.tsx +0 -87
  89. package/template/core/src/openpress/pagination.ts +0 -845
  90. package/template/packs/claude-document/document/chapters/01-document-shape/chapter.tsx +0 -30
  91. package/template/packs/claude-document/document/chapters/02-review-loop/chapter.tsx +0 -30
  92. /package/template/core/engine/{chrome-pdf.d.mts → output/chrome-pdf.d.mts} +0 -0
  93. /package/template/core/engine/{katex-assets.mjs → output/katex-assets.mjs} +0 -0
  94. /package/template/core/engine/{page-block.mjs → output/page-block.mjs} +0 -0
  95. /package/template/core/engine/{pdf-media.mjs → output/pdf-media.mjs} +0 -0
  96. /package/template/core/engine/{config.d.mts → runtime/config.d.mts} +0 -0
  97. /package/template/core/engine/{issue-report.mjs → runtime/issue-report.mjs} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for open-press — AI-first fixed-layout document workspaces.",
6
6
  "license": "MIT",
@@ -0,0 +1,126 @@
1
+ # Working on this open-press workspace
2
+
3
+ This directory is an **open-press workspace** scaffolded by
4
+ `@open-press/cli`. You (the agent) own:
5
+
6
+ - `document/` — chapters (MDX), components, theme, media. The actual content.
7
+ - `package.json` / `openpress.config.mjs` — project metadata + build config.
8
+ - `.agents/skills/` — agent skills installed by `npx skills` (auto-refreshed via `npx open-press upgrade`).
9
+
10
+ The rest of the tree is the open-press framework copied at scaffold time
11
+ (`engine/`, `src/`, `vite.config.ts`, etc). Treat it as vendored — don't
12
+ hand-edit unless the user explicitly asks; the next `npx open-press upgrade`
13
+ will rewrite it.
14
+
15
+ ## Quick reference
16
+
17
+ ```bash
18
+ npm run dev # workbench at http://127.0.0.1:5173/?dev=1
19
+ npm run openpress:validate # structural checks
20
+ npm run openpress:render # full render through chromium
21
+ npm run openpress:pdf # write document.pdf
22
+ npm run openpress:export # write public/openpress/document.json
23
+ npm run openpress:deploy # deploy via the configured adapter
24
+ npx open-press doctor # current vs latest version + pending migrations
25
+ npx open-press upgrade # apply the upgrade flow (see below)
26
+ ```
27
+
28
+ ## When the user asks to upgrade
29
+
30
+ Triggers: "升級 / 套件更新 / upgrade open-press / apply latest design /
31
+ update to vX.Y.Z" etc.
32
+
33
+ **Follow the upgrade SOP, do NOT manually diff template versions:**
34
+
35
+ 1. `npx open-press doctor` — confirm current vs latest, count pending
36
+ migrations.
37
+ 2. Ask the user "go ahead?" — briefly mention what will change (deps,
38
+ skills, migrations to read).
39
+ 3. `npx open-press upgrade` — refreshes `@open-press/core`, refreshes
40
+ installed skills, **fetches migration notes for each pending version
41
+ into `.openpress/migrations/<version>.md`** and lists the paths.
42
+ 4. For each migration file listed, read it fully. Each has a
43
+ `Document-level changes` section with `rg` find + rewrite rules.
44
+ Apply to `document/` with user confirmation.
45
+ 5. Verify:
46
+ ```bash
47
+ npm run openpress:validate
48
+ npm run openpress:render
49
+ ```
50
+ Fix anything broken using the migration notes.
51
+ 6. Report to the user: starting version → ending version, what was
52
+ applied, anything that needed manual judgement.
53
+
54
+ **Anti-pattern**: running `npx @open-press/cli@latest init` somewhere
55
+ and manually diffing against the workspace. The migration notes are the
56
+ authoritative source for what changed; fresh templates ship default
57
+ content that does not apply to a customised workspace.
58
+
59
+ ## When the user says "I changed X but the page didn't update"
60
+
61
+ Common cause: **the reader renders from a static `public/openpress/document.json`,
62
+ not from your live MDX / theme files.** Vite Hot Reload covers React UI
63
+ chrome (workbench panels, inspector, navigation) but it does **not**
64
+ regenerate `document.json`. So edits to:
65
+
66
+ - `document/chapters/**/*.mdx` (prose)
67
+ - `document/index.tsx` (Press tree, Cover/BackCover JSX)
68
+ - `document/components/**/*.tsx` (Page, openers, custom components)
69
+ - `document/theme/**` style files that affect pagination capacity
70
+ - `openpress.config.mjs` metadata (title, captionNumbering, …)
71
+
72
+ …all need a re-export before the workbench / public viewer reflect
73
+ the change:
74
+
75
+ ```bash
76
+ npm run openpress:export # regenerate public/openpress/document.json
77
+ # then refresh the browser
78
+ ```
79
+
80
+ Quick rules of thumb:
81
+
82
+ - Pure CSS edits under `document/theme/` that don't move blocks → HMR
83
+ is enough (CSS is hot-replaced).
84
+ - Anything that affects content, pagination, or metadata → re-export.
85
+ - `npm run openpress:render` is `export` + extra asset sync; either
86
+ works to refresh the JSON.
87
+
88
+ **Agent SOP**: after applying any non-CSS edit to `document/`, run
89
+ `npm run openpress:export` before telling the user "done". If they ask
90
+ "why didn't my change show up?", check whether `document.json` was
91
+ regenerated since the edit.
92
+
93
+ ## When the user reports a render / paginate issue
94
+
95
+ Press Tree paginates at build time. Common things to check:
96
+
97
+ 1. `npm run openpress:export` then inspect
98
+ `public/openpress/document.json` for `source.warnings` (chain
99
+ overflow, missing chains, etc.).
100
+ 2. `npm run openpress:validate` for structural issues
101
+ (missing entries, broken anchors).
102
+ 3. `npm run dev` and use the workbench inspector to find which MDX
103
+ block / Frame element is misbehaving — comments and inline
104
+ annotations work directly from there.
105
+
106
+ ## Skills
107
+
108
+ This workspace ships agent skills under `.agents/skills/`. If your
109
+ platform supports skills (Claude Code, Cursor, Codex, Cline, Gemini
110
+ CLI, …), prefer invoking them over re-reading this file:
111
+
112
+ - `openpress` — operate the workspace (CLI, validate, export, render,
113
+ PDF, deploy, search/replace, comments, upgrades, routing).
114
+ - `openpress-writing` — writing-time rules for MDX prose.
115
+ - `openpress-design` — theme tokens, layout, page surfaces.
116
+ - `openpress-deploy` — deployment workflows.
117
+ - `openpress-init` — scaffolding new workspaces.
118
+ - Plus any style-pack-specific skills installed by the user.
119
+
120
+ Skills are kept in sync by `npx skills upgrade` (run automatically
121
+ inside `npx open-press upgrade`).
122
+
123
+ ## Reporting issues
124
+
125
+ - Issues / questions: https://github.com/quan0715/open-press/issues
126
+ - Source: https://github.com/quan0715/open-press
@@ -1,5 +1,88 @@
1
1
  # @open-press/core
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Measurement pipeline + pagination fixes:
8
+
9
+ - **Measurement**: wait on `document.fonts.ready`, image `load`/`error` + `decode()`,
10
+ and two `requestAnimationFrame` ticks before sampling block heights so figures
11
+ no longer under-measure on cold loads.
12
+ - **Measurement**: inline relative `media/`, `./media/`, and `/openpress/media/`
13
+ image sources during the SSR measurement pass (previously only the absolute
14
+ `/openpress/media/...` form was rewritten, leaving relative refs as broken).
15
+ - **MDX compile**: split bullet/numbered lists into per-item paginable blocks
16
+ so long lists can break across pages without losing ordered numbering.
17
+ - **Debug**: new `OPENPRESS_DEBUG_ALLOC` env var prints per-iteration allocator
18
+ state (mdxArea capacities, block heights, pagination hints, warnings).
19
+ - **Academic-paper starter**: `<MdxArea overflow="extend">` on the body and the
20
+ single-column `.reader-page--content .page-body` override removed so content
21
+ paginates naturally with the new allocator.
22
+
23
+ ## 0.7.0
24
+
25
+ ### Minor Changes
26
+
27
+ - 718d2d1: **Press Tree render architecture** — full refresh of the React export pipeline (clean break, no v0.5 compatibility).
28
+
29
+ The render kernel no longer knows about `cover`, `toc`, `chapter`, or `back-cover` as built-in concepts. Workspaces describe their document as a React tree using three primitives:
30
+
31
+ - `Press` — root composition boundary.
32
+ - `Frame` — a single fixed-layout surface (replaces `BasePage` and friends).
33
+ - `MdxArea` — a measurable slot consuming a content chain, with `overflow="extend|truncate|error"` control.
34
+
35
+ Sources are now declarative descriptors:
36
+
37
+ ```tsx
38
+ export const sources = {
39
+ story: mdxSource({ preset: "section-folders", root: "chapters" }),
40
+ };
41
+
42
+ export default function MyPress() {
43
+ return (
44
+ <Press>
45
+ <Cover />
46
+ <Toc source="story" />
47
+ <Sections source="story" page={Page} />
48
+ <BackCover />
49
+ </Press>
50
+ );
51
+ }
52
+ ```
53
+
54
+ Three `mdxSource()` presets: `section-folders` (existing convention), `section-files` (flat file-per-section), `file-list` (explicit ordering).
55
+
56
+ Manuscript helpers (`Toc`, `Sections`, `Chapters` alias) ship in `@open-press/core/manuscript`. `mdxSource()` lives in `@open-press/core/mdx`. Subpath exports keep the public surface tight without committing to separate npm packages.
57
+
58
+ `Toc` is implemented as a manuscript helper, not a core kernel special case. Registered sources generate a synthetic `toc:<sourceId>` chain; `TocArea` consumes it with the same allocation path as `MdxArea`.
59
+
60
+ Reader-side pagination is removed. The export pipeline writes final frame HTML into `document.json`; the reader displays that HTML and no longer mutates headings/captions, injects footers, or reflows pages at runtime. Page shell belongs to workspace components.
61
+
62
+ MDX source resolution now derives manuscript TOC entries from actual `##` / `###` headings, not folder slugs. Heading numbering is written during export as `data-chapter="01"` / `data-section="1.1"` attributes so themes can render numbering with CSS without reader-side mutation.
63
+
64
+ **Removed (no compatibility layer):**
65
+
66
+ - `BasePage`, `BaseCoverPage`, `BaseTocPage`, `BaseContentPage`, `BaseBackCoverPage`.
67
+ - Legacy named exports (`cover`, `toc`, `backCover`) from `document/index.tsx`.
68
+ - The `migrate-to-react` CLI command.
69
+ - Implicit chapter discovery as the only source mechanism.
70
+ - Legacy `chapter.tsx` meta/opener auto-discovery. Section openers are explicit workspace components in the Press tree.
71
+
72
+ The top-level purity gate remains: `config` must be data, `sources` must be pure `mdxSource()` descriptors, and filesystem/network/process side effects are rejected before module execution. Default-exported function bodies can contain normal React component logic, including hooks and `.map()`.
73
+
74
+ All `<Frame>` instances require a stable `frameKey`; source roots and file-list entries must stay inside `document/`.
75
+
76
+ **Workspace data attributes:**
77
+
78
+ - `data-frame-role` (new, opaque role like `"manuscript.content"`).
79
+ - `data-page-kind` (derived from role's last segment — reader CSS keeps using this).
80
+ - `data-section-id` replaces `data-chapter-slug` for section-scoped CSS.
81
+
82
+ **Migration:** Workspaces written for v0.5.x must rewrite `document/index.tsx` to default-export a Press component. Pre-1.0 minor bump is acceptable per repo policy; no production deployments exist to break.
83
+
84
+ See `docs/superpowers/specs/2026-05-23-press-tree-render-architecture-design.md` for full design rationale.
85
+
3
86
  ## 0.6.0
4
87
 
5
88
  ### Minor Changes
@@ -40,6 +123,7 @@
40
123
  - 0169cba: Agent-driven upgrade flow.
41
124
 
42
125
  **New commands:**
126
+
43
127
  - `npx open-press doctor` — diagnose workspace against latest framework state. Reports `@open-press/core` version vs npm latest, installed skill count, and any pending `docs/migrations/<version>.md` notes between current and latest. `--json` for machine-readable output, `--no-cache` to bypass the 24h cache. Always exits 0 (informational only).
44
128
 
45
129
  - `npx open-press upgrade` — orchestrate the upgrade. Runs `npm update @open-press/core` (when the workspace declares the dep) and `npx skills upgrade`, then surfaces the list of migration notes for the agent to read. **Does not auto-edit `document/` content** — the agent reads the surfaced `docs/migrations/<version>.md` notes and proposes edits to the user with confirmation. Use `--dry-run` to preview, `--no-deps` / `--no-skills` to target one layer.
@@ -49,6 +133,7 @@
49
133
  `open-press dev` now runs `doctor` before starting Vite. When the workspace is behind, a single line prints: `○ open-press: @open-press/core 0.4.0 → 0.5.0 · 1 migration note(s) — run npx open-press doctor for details.` Cached for 24h, network failure is silent, never blocks dev.
50
134
 
51
135
  **Migration docs:**
136
+
52
137
  - New `docs/migrations/_template.md` — each release with breaking changes ships a `docs/migrations/<version>.md` file with sections the agent reads.
53
138
  - New `docs/migrations/0.4.0.md` — backfilled. Documents the SKILL fold (no document or CLI changes).
54
139
 
@@ -65,6 +150,7 @@
65
150
  ### Minor Changes
66
151
 
67
152
  - 3cb4939: Consolidate internal skills (13 → 11).
153
+
68
154
  - `openpress-update` folded into `openpress` as an "Updating An Existing Workspace" section. The release-upgrade flow, pre-flight checks, breaking-change reference, and do-not list are now part of the system-operation skill where they naturally belong.
69
155
  - `openpress-document-hierarchy` folded into `openpress-writing` as a "Hierarchy" section. Hierarchy decisions (H2/H3/H4 model, TOC depth, appendix placement, H4 granularity) and prose decisions happen in the same workflow; one skill, one routing decision.
70
156
  - `references/data-structures-outline.md` moved from the hierarchy skill into `openpress-writing/references/`.
@@ -81,4 +167,4 @@
81
167
 
82
168
  **@open-press/cli** (new): scaffolder for open-press workspaces. Run `npx @open-press/cli init <target> --pack <pack>` to create a fixed-layout document workspace from a bundled template. Supports `editorial-monograph` and `claude-document` style packs, metadata flags, and AI-agent skill installation under `.claude/skills/` and `.agents/skills/`.
83
169
 
84
- **@open-press/core** (new): framework runtime, CLI engine, render pipeline, and base page primitives (BasePage, BaseCoverPage, BaseTocPage, BaseBackCoverPage, BaseFigure, BaseCallout). Consumed transitively by workspaces scaffolded via `@open-press/cli`. Exposes the `open-press` bin (dev / build / preview / validate / pdf / deploy / export).
170
+ **@open-press/core** (new): framework runtime, CLI engine, render pipeline, and document primitives. Consumed transitively by workspaces scaffolded via `@open-press/cli`. Exposes the `open-press` bin (dev / build / preview / validate / pdf / deploy / export).
@@ -1,6 +1,6 @@
1
1
  # @open-press/core
2
2
 
3
- Framework runtime, CLI engine, and page primitives for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
3
+ Framework runtime, CLI engine, and Press Tree primitives for [open-press](https://github.com/quan0715/open-press) — an AI-first fixed-layout document workspace.
4
4
 
5
5
  Most users do **not** install this package directly. Instead, scaffold a workspace with the CLI:
6
6
 
@@ -20,15 +20,19 @@ npm install @open-press/core
20
20
 
21
21
  ```tsx
22
22
  import {
23
- BasePage,
24
- BaseCoverPage,
25
- BaseTocPage,
26
- BaseBackCoverPage,
23
+ Press,
24
+ Frame,
25
+ MdxArea,
27
26
  BaseFigure,
28
27
  BaseCallout,
29
28
  } from "@open-press/core";
29
+
30
+ import { mdxSource } from "@open-press/core/mdx";
31
+ import { Sections, Toc } from "@open-press/core/manuscript";
30
32
  ```
31
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.
35
+
32
36
  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).
33
37
 
34
38
  ## License
@@ -6,7 +6,6 @@ import * as doctorCmd from "./commands/doctor.mjs";
6
6
  import * as exportCmd from "./commands/export.mjs";
7
7
  import * as initCmd from "./commands/init.mjs";
8
8
  import * as inspectCmd from "./commands/inspect.mjs";
9
- import * as migrateToReactCmd from "./commands/migrate-to-react.mjs";
10
9
  import * as pdfCmd from "./commands/pdf.mjs";
11
10
  import * as previewCmd from "./commands/preview.mjs";
12
11
  import * as replaceCmd from "./commands/replace.mjs";
@@ -16,13 +15,12 @@ import * as typecheckCmd from "./commands/typecheck.mjs";
16
15
  import * as upgradeCmd from "./commands/upgrade.mjs";
17
16
  import * as validateCmd from "./commands/validate.mjs";
18
17
  import { parseOptions } from "./commands/_shared.mjs";
19
- import { loadConfig } from "./config.mjs";
18
+ import { loadConfig } from "./runtime/config.mjs";
20
19
  import { listStylePackSkills } from "./init.mjs";
21
- import { discoverWorkspace } from "./validation.mjs";
20
+ import { discoverWorkspace } from "./runtime/validation.mjs";
22
21
 
23
22
  const COMMANDS = {
24
23
  init: initCmd,
25
- "migrate-to-react": migrateToReactCmd,
26
24
  validate: validateCmd,
27
25
  inspect: inspectCmd,
28
26
  search: searchCmd,
@@ -79,7 +77,6 @@ async function printHelp() {
79
77
 
80
78
  Commands:
81
79
  init <target> [--skill <name>] [--force]
82
- migrate-to-react [path] [--dry-run] [--force] [--json]
83
80
  validate
84
81
  inspect [--json] [--no-build] [--dry-run]
85
82
  search [path] <query> [--json] [--scope content|all]
@@ -2,14 +2,14 @@ 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 "../chrome-pdf.mjs";
6
- import { loadConfig, publicPdfHref } from "../config.mjs";
5
+ import { printUrlToPdf, stopChildProcess, waitForPrintReady } from "../output/chrome-pdf.mjs";
6
+ import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
7
7
  import { exportDocument } from "../document-export.mjs";
8
- import { optimizePdfMediaForStaticRoot } from "../pdf-media.mjs";
8
+ import { optimizePdfMediaForStaticRoot } from "../output/pdf-media.mjs";
9
9
 
10
10
  export const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
11
  export const CLI_ENTRY = path.join(ENGINE_DIR, "cli.mjs");
12
- export const STATIC_SERVER = path.join(ENGINE_DIR, "static-server.mjs");
12
+ export const STATIC_SERVER = path.join(ENGINE_DIR, "output", "static-server.mjs");
13
13
 
14
14
  export function parseOptions(argv) {
15
15
  const options = {};
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { deploySync } from "../deploy-sync.mjs";
2
+ import { deploySync } from "../output/deploy-sync.mjs";
3
3
  import { CLI_ENTRY, buildReactPdf, formatNodeScriptCommand, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
4
4
 
5
5
  export async function run({ root, config, options, recurse }) {
@@ -1,5 +1,5 @@
1
- import { inspectWorkspace } from "../inspection.mjs";
2
- import { exitCodeForIssueReport } from "../issue-report.mjs";
1
+ import { inspectWorkspace } from "../runtime/inspection.mjs";
2
+ import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
3
3
 
4
4
  export async function run({ root, config, options, recurse }) {
5
5
  const host = options.host ?? "127.0.0.1";
@@ -10,7 +10,7 @@ export async function run({ root, config, options, recurse }) {
10
10
  if (!options.noBuild) {
11
11
  console.log("Command: node engine/cli.mjs render . --renderer react");
12
12
  }
13
- console.log(`Command: node engine/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
13
+ console.log(`Command: node engine/output/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
14
14
  console.log(`Chrome inspection URL: ${url}`);
15
15
  return 0;
16
16
  }
@@ -1,4 +1,4 @@
1
- import { replaceSourceText } from "../source-text-tools.mjs";
1
+ import { replaceSourceText } from "../runtime/source-text-tools.mjs";
2
2
 
3
3
  export async function run({ config, options }) {
4
4
  const args = replaceArgsFromOptions(options);
@@ -1,4 +1,4 @@
1
- import { searchSourceText } from "../source-text-tools.mjs";
1
+ import { searchSourceText } from "../runtime/source-text-tools.mjs";
2
2
 
3
3
  export async function run({ config, options }) {
4
4
  const query = searchQueryFromOptions(options);
@@ -1,9 +1,15 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { diagnose } from "./doctor.mjs";
5
5
  import { runCommand } from "./_shared.mjs";
6
6
 
7
+ // Migration notes live in the framework repo, not in scaffolded workspaces.
8
+ // `npx open-press upgrade` fetches the notes for each pending version and
9
+ // caches them under `.openpress/migrations/` so agents can read locally.
10
+ const MIGRATION_REMOTE_BASE = "https://raw.githubusercontent.com/quan0715/open-press/main/docs/migrations";
11
+ const MIGRATION_CACHE_DIR = path.join(".openpress", "migrations");
12
+
7
13
  export async function run({ root, options }) {
8
14
  const dryRun = Boolean(options?.dryRun);
9
15
  const skipSkills = Boolean(options?.noSkills);
@@ -85,7 +91,11 @@ export async function run({ root, options }) {
85
91
  process.stdout.write(" (no migration docs in this version range)\n\n");
86
92
  } else {
87
93
  for (const m of migrationContents) {
88
- process.stdout.write(` ─ ${m.path}\n`);
94
+ if (m.path) {
95
+ process.stdout.write(` ─ ${m.path}${m.fetched ? " (fetched from github)" : ""}\n`);
96
+ } else {
97
+ process.stdout.write(` ─ ${m.version}.md (not found locally or on github — check the repo manually)\n`);
98
+ }
89
99
  }
90
100
  process.stdout.write(
91
101
  "\nAgent: open each file, identify document-level changes, grep document/ for affected patterns, propose edits before applying.\n",
@@ -107,11 +117,43 @@ async function hasCoreDep(root) {
107
117
 
108
118
  async function loadMigrations(root, versions) {
109
119
  const results = [];
120
+ const cacheDir = path.join(root, MIGRATION_CACHE_DIR);
121
+ await mkdir(cacheDir, { recursive: true });
122
+
110
123
  for (const v of versions) {
111
- const p = path.join(root, "docs", "migrations", `${v}.md`);
112
- if (existsSync(p)) {
113
- results.push({ version: v, path: path.relative(root, p) });
124
+ // Framework repo has docs/migrations/ at root — prefer local if present
125
+ // (covers the open-press framework repo itself acting as a workspace).
126
+ const localDocsPath = path.join(root, "docs", "migrations", `${v}.md`);
127
+ if (existsSync(localDocsPath)) {
128
+ results.push({ version: v, path: path.relative(root, localDocsPath), fetched: false });
129
+ continue;
130
+ }
131
+
132
+ // Otherwise fetch from GitHub raw and cache to .openpress/migrations/.
133
+ const cachedPath = path.join(cacheDir, `${v}.md`);
134
+ if (existsSync(cachedPath)) {
135
+ results.push({ version: v, path: path.relative(root, cachedPath), fetched: false });
136
+ continue;
137
+ }
138
+
139
+ const body = await fetchMigration(v);
140
+ if (body) {
141
+ await writeFile(cachedPath, body, "utf8");
142
+ results.push({ version: v, path: path.relative(root, cachedPath), fetched: true });
143
+ } else {
144
+ results.push({ version: v, path: null, fetched: false });
114
145
  }
115
146
  }
116
147
  return results;
117
148
  }
149
+
150
+ async function fetchMigration(version) {
151
+ const url = `${MIGRATION_REMOTE_BASE}/${version}.md`;
152
+ try {
153
+ const res = await fetch(url, { headers: { Accept: "text/plain" } });
154
+ if (!res.ok) return null;
155
+ return await res.text();
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
@@ -1,5 +1,5 @@
1
- import { validateWorkspace } from "../validation.mjs";
2
- import { exitCodeForIssueReport } from "../issue-report.mjs";
1
+ import { validateWorkspace } from "../runtime/validation.mjs";
2
+ import { exitCodeForIssueReport } from "../runtime/issue-report.mjs";
3
3
 
4
4
  export async function run({ root, options }) {
5
5
  const report = await validateWorkspace(root);
@@ -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; run `node engine/cli.mjs migrate-to-react .` before exporting.",
13
+ "React/MDX document entry not found. Expected document/index.tsx with a Press default export before exporting.",
14
14
  );
15
15
  }
@@ -203,8 +203,7 @@ export async function waitForPrintReady(client) {
203
203
  awaitPromise: true,
204
204
  expression: `Promise.resolve().then(async () => {
205
205
  const root = document.querySelector('[data-openpress-print-document="true"]');
206
- const ready = root?.getAttribute('data-openpress-pagination') === 'ready';
207
- if (!ready) return 0;
206
+ if (!root || root.querySelectorAll('.openpress-html-page').length === 0) return 0;
208
207
 
209
208
  await document.fonts?.ready;
210
209
  await Promise.all(Array.from(document.images).map(async (img) => {
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadConfig } from "./config.mjs";
4
- import { copyDirectory } from "./file-utils.mjs";
3
+ import { loadConfig } from "../runtime/config.mjs";
4
+ import { copyDirectory } from "../runtime/file-utils.mjs";
5
5
 
6
6
  export async function deploySync(root, sourceDir, deployDir) {
7
7
  const config = await loadConfig(root);
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { copyDirectory } from "./file-utils.mjs";
3
+ import { copyDirectory } from "../runtime/file-utils.mjs";
4
4
 
5
5
  export async function copyThemeFonts(root, publicOutputDir, config) {
6
6
  const themeDir = config?.paths?.themeDir ?? path.join(path.resolve(root), "theme");
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadConfig } from "./config.mjs";
4
- import { copyDirectory, writeComponentsCss, writeContentCss } from "./file-utils.mjs";
3
+ import { loadConfig } from "../runtime/config.mjs";
4
+ import { copyDirectory, writeComponentsCss, writeContentCss } from "../runtime/file-utils.mjs";
5
5
  import { copyThemeFonts } from "./fonts.mjs";
6
6
  import { copyKatexFonts } from "./katex-assets.mjs";
7
7
 
@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
- import { loadConfig, publicPdfHref } from "./config.mjs";
6
- import { handleProjectAssetRequest } from "./react/project-asset-endpoint.mjs";
5
+ import { loadConfig, publicPdfHref } from "../runtime/config.mjs";
6
+ import { handleProjectAssetRequest } from "../react/project-asset-endpoint.mjs";
7
7
 
8
8
  const [rootArg = "dist", ...rest] = process.argv.slice(2);
9
9
  const host = valueAfter(rest, "--host") ?? "127.0.0.1";
@@ -0,0 +1,73 @@
1
+ const DEFAULT_CAPTION_NUMBERING = {
2
+ figure: "Figure",
3
+ table: "Table",
4
+ separator: " ",
5
+ };
6
+
7
+ export function normalizeCaptionNumbering(value = {}) {
8
+ const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
9
+ return {
10
+ figure: stringOption(input.figure, DEFAULT_CAPTION_NUMBERING.figure),
11
+ table: stringOption(input.table, DEFAULT_CAPTION_NUMBERING.table),
12
+ separator: typeof input.separator === "string" ? input.separator : DEFAULT_CAPTION_NUMBERING.separator,
13
+ };
14
+ }
15
+
16
+ export function createCaptionNumberingState() {
17
+ return {
18
+ figure: 0,
19
+ table: 0,
20
+ seenTables: new Set(),
21
+ };
22
+ }
23
+
24
+ export function numberCaptionsInHtml(html, numbering, state = createCaptionNumberingState()) {
25
+ if (!html) return html;
26
+ const options = normalizeCaptionNumbering(numbering);
27
+ let out = String(html);
28
+ out = numberTableCaptions(out, options, state);
29
+ out = numberFigureCaptions(out, options, state);
30
+ return out;
31
+ }
32
+
33
+ function numberTableCaptions(html, options, state) {
34
+ return html.replace(/<table\b([^>]*)>([\s\S]*?<caption\b([^>]*)>)([\s\S]*?)(<\/caption>[\s\S]*?<\/table>)/g, (match, tableAttrs, beforeCaptionText, captionAttrs, captionText, afterCaptionText) => {
35
+ if (captionText.includes("data-openpress-caption-label=")) return match;
36
+ const tableId = attrValue(tableAttrs, "data-openpress-table-id");
37
+ if (tableId && state.seenTables.has(tableId)) return match;
38
+ if (tableId) state.seenTables.add(tableId);
39
+ state.table += 1;
40
+ const label = captionLabel(options.table, state.table, options.separator);
41
+ return `<table${tableAttrs}>${beforeCaptionText}${captionLabelSpan("table", state.table, label)} ${captionText}${afterCaptionText}`;
42
+ });
43
+ }
44
+
45
+ function numberFigureCaptions(html, options, state) {
46
+ return html.replace(/<figure\b([^>]*)>([\s\S]*?<figcaption\b([^>]*)>)([\s\S]*?)(<\/figcaption>[\s\S]*?<\/figure>)/g, (match, figureAttrs, beforeCaptionText, captionAttrs, captionText, afterCaptionText) => {
47
+ if (captionText.includes("data-openpress-caption-label=")) return match;
48
+ state.figure += 1;
49
+ const label = captionLabel(options.figure, state.figure, options.separator);
50
+ return `<figure${figureAttrs}>${beforeCaptionText}${captionLabelSpan("figure", state.figure, label)} ${captionText}${afterCaptionText}`;
51
+ });
52
+ }
53
+
54
+ function captionLabel(noun, number, separator) {
55
+ return `${noun}${separator}${number}`;
56
+ }
57
+
58
+ function captionLabelSpan(kind, number, label) {
59
+ return `<span class="openpress-caption-label" data-openpress-caption-label="${kind}" data-openpress-caption-number="${number}">${escapeHtml(label)}</span>`;
60
+ }
61
+
62
+ function attrValue(attrs, name) {
63
+ const pattern = new RegExp(`${name}=(["'])(.*?)\\1`);
64
+ return attrs.match(pattern)?.[2] ?? "";
65
+ }
66
+
67
+ function stringOption(value, fallback) {
68
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
69
+ }
70
+
71
+ function escapeHtml(value) {
72
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
73
+ }