@open-press/cli 0.3.0 → 0.5.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 (94) hide show
  1. package/dist/cli.js +20 -37
  2. package/package.json +1 -1
  3. package/template/core/CHANGELOG.md +43 -0
  4. package/template/core/engine/cli.mjs +6 -0
  5. package/template/core/engine/commands/_shared.mjs +9 -2
  6. package/template/core/engine/commands/deploy.mjs +3 -3
  7. package/template/core/engine/commands/dev.mjs +25 -2
  8. package/template/core/engine/commands/doctor.mjs +229 -0
  9. package/template/core/engine/commands/pdf.mjs +3 -3
  10. package/template/core/engine/commands/preview.mjs +4 -4
  11. package/template/core/engine/commands/upgrade.mjs +117 -0
  12. package/template/core/package.json +3 -1
  13. package/template/core/vite.config.ts +26 -11
  14. package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/chapters/03-agent-skills-contributors/content/01-agent-skills-contributors.mdx +2 -3
  15. package/template/core/.turbo/turbo-test.log +0 -341
  16. package/template/skills/chinese-ai-writing-polish/SKILL.md +0 -195
  17. package/template/skills/claude-document/SKILL.md +0 -66
  18. package/template/skills/editorial-monograph/SKILL.md +0 -73
  19. package/template/skills/openpress/SKILL.md +0 -114
  20. package/template/skills/openpress/references/cli-commands.md +0 -31
  21. package/template/skills/openpress/references/local-review.md +0 -43
  22. package/template/skills/openpress-deploy/SKILL.md +0 -69
  23. package/template/skills/openpress-deploy/references/cloudflare-pages.md +0 -51
  24. package/template/skills/openpress-design/SKILL.md +0 -51
  25. package/template/skills/openpress-design/references/pdf-safe-css.md +0 -29
  26. package/template/skills/openpress-design/references/responsive-fixed-layout.md +0 -48
  27. package/template/skills/openpress-design/references/theme-and-components.md +0 -77
  28. package/template/skills/openpress-diagram-drawing/SKILL.md +0 -44
  29. package/template/skills/openpress-diagram-drawing/references/diagram-patterns.md +0 -93
  30. package/template/skills/openpress-document-hierarchy/SKILL.md +0 -81
  31. package/template/skills/openpress-document-hierarchy/agents/openai.yaml +0 -4
  32. package/template/skills/openpress-document-hierarchy/references/data-structures-outline.md +0 -115
  33. package/template/skills/openpress-init/SKILL.md +0 -84
  34. package/template/skills/openpress-style-pack-contributor/SKILL.md +0 -62
  35. package/template/skills/openpress-style-pack-contributor/references/starter-contract.md +0 -49
  36. package/template/skills/openpress-update/SKILL.md +0 -88
  37. package/template/skills/openpress-writing/SKILL.md +0 -68
  38. package/template/skills/openpress-writing/references/source-and-writing-rules.md +0 -120
  39. package/template/skills/teaching-notes-writing/SKILL.md +0 -54
  40. package/template/skills/teaching-notes-writing/references/programming.md +0 -65
  41. package/template/skills/teaching-notes-writing/references/teaching-patterns.md +0 -60
  42. /package/template/{skills/claude-document/starter → packs/claude-document}/document/chapters/01-document-shape/chapter.tsx +0 -0
  43. /package/template/{skills/claude-document/starter → packs/claude-document}/document/chapters/01-document-shape/content/01-document-shape.mdx +0 -0
  44. /package/template/{skills/claude-document/starter → packs/claude-document}/document/chapters/02-review-loop/chapter.tsx +0 -0
  45. /package/template/{skills/claude-document/starter → packs/claude-document}/document/chapters/02-review-loop/content/01-review-loop.mdx +0 -0
  46. /package/template/{skills/claude-document/starter → packs/claude-document}/document/components/ChapterOpenerVisual.tsx +0 -0
  47. /package/template/{skills/claude-document/starter → packs/claude-document}/document/components/Page.tsx +0 -0
  48. /package/template/{skills/claude-document/starter → packs/claude-document}/document/design.md +0 -0
  49. /package/template/{skills/claude-document/starter → packs/claude-document}/document/index.tsx +0 -0
  50. /package/template/{skills/claude-document/starter → packs/claude-document}/document/media/README.md +0 -0
  51. /package/template/{skills/claude-document/starter → packs/claude-document}/document/openpress.config.mjs +0 -0
  52. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/README.md +0 -0
  53. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/base/page-contract.css +0 -0
  54. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/base/print.css +0 -0
  55. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/base/typography.css +0 -0
  56. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/fonts.css +0 -0
  57. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/page-surfaces/back-cover.css +0 -0
  58. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/page-surfaces/chapter-opener.css +0 -0
  59. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/page-surfaces/cover.css +0 -0
  60. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/page-surfaces/toc.css +0 -0
  61. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/patterns/_chart-frame.css +0 -0
  62. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/patterns/figure-grid.css +0 -0
  63. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/patterns/table-utilities.css +0 -0
  64. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/shell/reader-controls.css +0 -0
  65. /package/template/{skills/claude-document/starter → packs/claude-document}/document/theme/tokens.css +0 -0
  66. /package/template/{skills/claude-document/starter → packs/claude-document}/openpress.config.mjs +0 -0
  67. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/chapters/01-product-and-use-cases/content/01-product-and-use-cases.mdx +0 -0
  68. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/chapters/02-workflow/content/01-workflow.mdx +0 -0
  69. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/chapters/04-validation-deploy/content/01-validation-deploy.mdx +0 -0
  70. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/components/ChapterOpenerVisual/index.tsx +0 -0
  71. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/components/Page.tsx +0 -0
  72. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/components/TokenSwatchGrid/index.tsx +0 -0
  73. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/components/TokenSwatchGrid/style.css +0 -0
  74. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/components/TypeSpecimen/index.tsx +0 -0
  75. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/components/TypeSpecimen/style.css +0 -0
  76. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/design.md +0 -0
  77. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/index.tsx +0 -0
  78. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/media/README.md +0 -0
  79. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/openpress.config.mjs +0 -0
  80. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/README.md +0 -0
  81. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/base/page-contract.css +0 -0
  82. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/base/print.css +0 -0
  83. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/base/typography.css +0 -0
  84. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/fonts.css +0 -0
  85. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/page-surfaces/back-cover.css +0 -0
  86. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/page-surfaces/chapter-opener.css +0 -0
  87. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/page-surfaces/cover.css +0 -0
  88. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/page-surfaces/toc.css +0 -0
  89. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/patterns/_chart-frame.css +0 -0
  90. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/patterns/figure-grid.css +0 -0
  91. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/patterns/table-utilities.css +0 -0
  92. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/shell/reader-controls.css +0 -0
  93. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/document/theme/tokens.css +0 -0
  94. /package/template/{skills/editorial-monograph/starter → packs/editorial-monograph}/openpress.config.mjs +0 -0
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import process2 from "process";
6
6
  // src/init.ts
7
7
  import { spawn } from "child_process";
8
8
  import { existsSync } from "fs";
9
- import { cp, mkdir as mkdir2, readdir, rm as rm2 } from "fs/promises";
9
+ import { cp, mkdir as mkdir2, rm as rm2 } from "fs/promises";
10
10
  import path2 from "path";
11
11
  import process from "process";
12
12
  import { fileURLToPath } from "url";
@@ -23,8 +23,8 @@ async function pathIsEmpty(target) {
23
23
  try {
24
24
  const s = await stat(target);
25
25
  if (!s.isDirectory()) return false;
26
- const { readdir: readdir2 } = await import("fs/promises");
27
- const entries = await readdir2(target);
26
+ const { readdir } = await import("fs/promises");
27
+ const entries = await readdir(target);
28
28
  return entries.length === 0;
29
29
  } catch {
30
30
  return true;
@@ -81,10 +81,11 @@ async function patchPackageJsonName(packagePath, newName) {
81
81
 
82
82
  // src/init.ts
83
83
  var KNOWN_PACKS = ["editorial-monograph", "claude-document"];
84
+ var SKILLS_SOURCE = "quan0715/open-press";
84
85
  var __dirname = path2.dirname(fileURLToPath(import.meta.url));
85
86
  var TEMPLATE_ROOT = path2.resolve(__dirname, "..", "template");
86
87
  var TEMPLATE_CORE = path2.join(TEMPLATE_ROOT, "core");
87
- var TEMPLATE_SKILLS = path2.join(TEMPLATE_ROOT, "skills");
88
+ var TEMPLATE_PACKS = path2.join(TEMPLATE_ROOT, "packs");
88
89
  async function init(options) {
89
90
  validatePack(options.pack);
90
91
  ensureTemplateBundled();
@@ -98,16 +99,14 @@ async function init(options) {
98
99
  log(`Applying style pack: ${options.pack}`);
99
100
  await rm2(docDest, { recursive: true, force: true });
100
101
  await mkdir2(docDest, { recursive: true });
101
- const packStarter = path2.join(TEMPLATE_SKILLS, options.pack, "starter", "document");
102
+ const packStarter = path2.join(TEMPLATE_PACKS, options.pack, "document");
102
103
  if (!existsSync(packStarter)) {
103
- throw new Error(`Style pack starter not found: ${packStarter}`);
104
+ throw new Error(`Style pack starter not found in bundle: ${packStarter}`);
104
105
  }
105
106
  await cp(packStarter, docDest, { recursive: true });
106
107
  } else {
107
108
  await mkdir2(docDest, { recursive: true });
108
109
  }
109
- log("Installing SKILL files\u2026");
110
- await installSkills(target);
111
110
  const pkgPath = path2.join(target, "package.json");
112
111
  if (existsSync(pkgPath)) {
113
112
  await patchPackageJsonName(pkgPath, path2.basename(target));
@@ -122,6 +121,13 @@ async function init(options) {
122
121
  author: options.author
123
122
  });
124
123
  }
124
+ log(`Installing agent skills via \`npx skills add ${SKILLS_SOURCE}\`\u2026`);
125
+ try {
126
+ await runInTarget(target, "npx", ["-y", "skills@latest", "add", SKILLS_SOURCE]);
127
+ } catch (err) {
128
+ log(`(skills install failed; you can retry manually with \`npx skills add ${SKILLS_SOURCE}\`)`);
129
+ log(` reason: ${err instanceof Error ? err.message : String(err)}`);
130
+ }
125
131
  if (options.install) {
126
132
  log("Installing dependencies (npm install)\u2026");
127
133
  await runInTarget(target, "npm", ["install"]);
@@ -142,33 +148,8 @@ async function init(options) {
142
148
  }
143
149
  printNextSteps(target, options);
144
150
  }
145
- async function installSkills(target) {
146
- const skillDirs = await readdir(TEMPLATE_SKILLS, { withFileTypes: true });
147
- const claudeRoot = path2.join(target, ".claude", "skills");
148
- const agentsRoot = path2.join(target, ".agents", "skills");
149
- await mkdir2(claudeRoot, { recursive: true });
150
- await mkdir2(agentsRoot, { recursive: true });
151
- for (const dir of skillDirs) {
152
- if (!dir.isDirectory()) continue;
153
- const source = path2.join(TEMPLATE_SKILLS, dir.name);
154
- const claudeTarget = path2.join(claudeRoot, dir.name);
155
- const agentsTarget = path2.join(agentsRoot, dir.name);
156
- await copySkillFiles(source, claudeTarget);
157
- await copySkillFiles(source, agentsTarget);
158
- }
159
- }
160
- async function copySkillFiles(source, dest) {
161
- await mkdir2(dest, { recursive: true });
162
- const entries = await readdir(source, { withFileTypes: true });
163
- for (const entry of entries) {
164
- if (entry.name === "starter" || entry.name === "agents") continue;
165
- const from = path2.join(source, entry.name);
166
- const to = path2.join(dest, entry.name);
167
- await cp(from, to, { recursive: true });
168
- }
169
- }
170
151
  function ensureTemplateBundled() {
171
- if (!existsSync(TEMPLATE_CORE) || !existsSync(TEMPLATE_SKILLS)) {
152
+ if (!existsSync(TEMPLATE_CORE) || !existsSync(TEMPLATE_PACKS)) {
172
153
  throw new Error(
173
154
  `Template not bundled at ${TEMPLATE_ROOT}. If running from source, run \`pnpm sync:template\` in packages/cli first.`
174
155
  );
@@ -216,7 +197,7 @@ function printNextSteps(target, options) {
216
197
  const rel = path2.relative(process.cwd(), target) || ".";
217
198
  const lines = [
218
199
  "",
219
- "\u2713 Done! Your open-press workspace is ready.",
200
+ "\u2713 Done. Your open-press workspace is ready.",
220
201
  "",
221
202
  "Next steps:",
222
203
  ` cd ${rel}`
@@ -229,9 +210,11 @@ function printNextSteps(target, options) {
229
210
  " # start the workbench:",
230
211
  " npm run dev",
231
212
  "",
232
- "Then open the local URL printed by vite (typically http://127.0.0.1:5173/?dev=1).",
213
+ "Then open the local URL printed by Vite (typically http://127.0.0.1:5173/?dev=1).",
214
+ "",
215
+ "Agent skills installed under .agents/skills/ (universal \u2014 read by Claude Code,",
216
+ `Cursor, Codex, Gemini CLI, etc.). Update later with: npx skills upgrade`,
233
217
  "",
234
- "AI agent skills are installed under .claude/skills/ and .agents/skills/.",
235
218
  "Edit content under document/chapters/, or ask your AI agent to help.",
236
219
  ""
237
220
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-press/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Scaffolder for open-press — AI-first fixed-layout document workspaces.",
6
6
  "license": "MIT",
@@ -1,5 +1,48 @@
1
1
  # @open-press/core
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0169cba: Agent-driven upgrade flow.
8
+
9
+ **New commands:**
10
+
11
+ - `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).
12
+
13
+ - `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.
14
+
15
+ **Dev startup notice:**
16
+
17
+ `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.
18
+
19
+ **Migration docs:**
20
+
21
+ - New `docs/migrations/_template.md` — each release with breaking changes ships a `docs/migrations/<version>.md` file with sections the agent reads.
22
+ - New `docs/migrations/0.4.0.md` — backfilled. Documents the SKILL fold (no document or CLI changes).
23
+
24
+ **SKILL update:**
25
+
26
+ `openpress` skill's "Updating An Existing Workspace" section rewritten around the new commands: detect (`doctor`), apply (`upgrade`), interpret migration notes, propose document edits with user confirmation. Concrete agent workflow + breaking-change reference table.
27
+
28
+ ### Patch Changes
29
+
30
+ - 931d4ac: Support framework root dogfood workspaces and correct CLI script paths outside the core package root.
31
+
32
+ ## 0.4.0
33
+
34
+ ### Minor Changes
35
+
36
+ - 3cb4939: Consolidate internal skills (13 → 11).
37
+
38
+ - `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.
39
+ - `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.
40
+ - `references/data-structures-outline.md` moved from the hierarchy skill into `openpress-writing/references/`.
41
+
42
+ Lower maintenance surface: 2 fewer SKILL.md files to keep in sync, ~5 fewer cross-references to police. No content lost — same rules, fewer files.
43
+
44
+ User impact: agents already in workspaces with `openpress-update` or `openpress-document-hierarchy` SKILL files installed should run `npx skills upgrade` to refresh the catalog.
45
+
3
46
  ## 0.3.0
4
47
 
5
48
  ### Minor Changes
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as deployCmd from "./commands/deploy.mjs";
4
4
  import * as devCmd from "./commands/dev.mjs";
5
+ import * as doctorCmd from "./commands/doctor.mjs";
5
6
  import * as exportCmd from "./commands/export.mjs";
6
7
  import * as initCmd from "./commands/init.mjs";
7
8
  import * as inspectCmd from "./commands/inspect.mjs";
@@ -12,6 +13,7 @@ import * as replaceCmd from "./commands/replace.mjs";
12
13
  import * as renderCmd from "./commands/render.mjs";
13
14
  import * as searchCmd from "./commands/search.mjs";
14
15
  import * as typecheckCmd from "./commands/typecheck.mjs";
16
+ import * as upgradeCmd from "./commands/upgrade.mjs";
15
17
  import * as validateCmd from "./commands/validate.mjs";
16
18
  import { parseOptions } from "./commands/_shared.mjs";
17
19
  import { loadConfig } from "./config.mjs";
@@ -32,6 +34,8 @@ const COMMANDS = {
32
34
  typecheck: typecheckCmd,
33
35
  pdf: pdfCmd,
34
36
  deploy: deployCmd,
37
+ doctor: doctorCmd,
38
+ upgrade: upgradeCmd,
35
39
  };
36
40
 
37
41
  const args = process.argv.slice(2);
@@ -87,6 +91,8 @@ Commands:
87
91
  typecheck
88
92
  pdf [--output <outputDir>/<pdf.filename>] [--no-build] [--dry-run]
89
93
  deploy --confirm [--dry-run]
94
+ doctor [--json] [--no-cache] # version + skill staleness check
95
+ upgrade [--dry-run] [--no-deps] [--no-skills] [--json] # apply updates; agent-driven
90
96
 
91
97
  Style packs available for \`init --skill\`: ${skillList}
92
98
  `);
@@ -7,8 +7,9 @@ import { loadConfig, publicPdfHref } from "../config.mjs";
7
7
  import { exportDocument } from "../document-export.mjs";
8
8
  import { optimizePdfMediaForStaticRoot } from "../pdf-media.mjs";
9
9
 
10
- const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
- const STATIC_SERVER = path.join(ENGINE_DIR, "static-server.mjs");
10
+ export const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ export const CLI_ENTRY = path.join(ENGINE_DIR, "cli.mjs");
12
+ export const STATIC_SERVER = path.join(ENGINE_DIR, "static-server.mjs");
12
13
 
13
14
  export function parseOptions(argv) {
14
15
  const options = {};
@@ -62,6 +63,12 @@ export function runCommand(commandName, commandArgs, cwd) {
62
63
  return result.status ?? 1;
63
64
  }
64
65
 
66
+ export function formatNodeScriptCommand(root, scriptPath) {
67
+ const relative = path.relative(root, scriptPath).replaceAll("\\", "/");
68
+ const displayPath = relative && !relative.startsWith("../") ? relative : scriptPath;
69
+ return `node ${displayPath}`;
70
+ }
71
+
65
72
  export async function buildReactStatic({ root, noBuild = false, recurse, silent = false }) {
66
73
  if (noBuild) return 0;
67
74
  if (!silent) {
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { deploySync } from "../deploy-sync.mjs";
3
- import { buildReactPdf, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
3
+ import { CLI_ENTRY, buildReactPdf, formatNodeScriptCommand, runCommand, writePdfStageDeployConfig } from "./_shared.mjs";
4
4
 
5
5
  export async function run({ root, config, options, recurse }) {
6
6
  if (config.deploy.requiresConfirmation === true && !options.confirm) {
@@ -12,9 +12,9 @@ export async function run({ root, config, options, recurse }) {
12
12
  const commitDirty = config.deploy.commitDirty;
13
13
  if (options.dryRun) {
14
14
  console.log("OpenPress deploy dry run");
15
- console.log("Command: node engine/cli.mjs render . --renderer react");
15
+ console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
16
16
  console.log(`Step: deploy-sync (copy ${config.outputDir} → ${source})`);
17
- console.log(`Command: node engine/cli.mjs pdf . --output ${source}/${config.pdf.filename}`);
17
+ console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} pdf . --output ${source}/${config.pdf.filename}`);
18
18
  console.log(`Step: write ${source}/openpress/deploy.json with deployment metadata`);
19
19
  console.log(`Command: npx wrangler pages deploy ${source}${projectName ? ` --project-name=${projectName}` : ""}${commitDirty ? " --commit-dirty=true" : ""}`);
20
20
  return 0;
@@ -1,5 +1,6 @@
1
1
  import { exportDocument } from "../document-export.mjs";
2
- import { runCommand } from "./_shared.mjs";
2
+ import { diagnose } from "./doctor.mjs";
3
+ import { CLI_ENTRY, formatNodeScriptCommand, runCommand } from "./_shared.mjs";
3
4
 
4
5
  export async function run({ root, options }) {
5
6
  const renderer = options.renderer ?? "react";
@@ -13,7 +14,7 @@ export async function run({ root, options }) {
13
14
  if (options.dryRun) {
14
15
  console.log(`OpenPress dev URL: ${url}`);
15
16
  if (!options.noBuild) {
16
- console.log("Command: node engine/cli.mjs export .");
17
+ console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} export .`);
17
18
  }
18
19
  console.log(`Command: npx vite --config vite.config.ts --host ${host} --port ${port}`);
19
20
  return 0;
@@ -21,6 +22,28 @@ export async function run({ root, options }) {
21
22
  if (!options.noBuild) {
22
23
  await exportDocument(root);
23
24
  }
25
+
26
+ // One-line update notice (24h cached, network failure is silent).
27
+ await printDoctorNoticeIfStale(root);
28
+
24
29
  console.log(`OpenPress dev: ${url}`);
25
30
  return runCommand("npx", ["vite", "--config", "vite.config.ts", "--host", host, "--port", port], root);
26
31
  }
32
+
33
+ async function printDoctorNoticeIfStale(root) {
34
+ try {
35
+ const report = await diagnose(root);
36
+ if (!report.stale) return;
37
+ const parts = [];
38
+ if (report.coreUpdateAvailable) {
39
+ parts.push(`@open-press/core ${report.coreVersion} → ${report.coreLatest}`);
40
+ }
41
+ if (report.pendingMigrations.length > 0) {
42
+ parts.push(`${report.pendingMigrations.length} migration note(s)`);
43
+ }
44
+ if (parts.length === 0) return;
45
+ console.log(`○ open-press: ${parts.join(" · ")} — run \`npx open-press doctor\` for details.`);
46
+ } catch {
47
+ // Doctor is informational only; never block dev.
48
+ }
49
+ }
@@ -0,0 +1,229 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
6
+ const CORE_PACKAGE = "@open-press/core";
7
+
8
+ export async function run({ root, options }) {
9
+ const json = Boolean(options?.json);
10
+ const noCache = Boolean(options?.noCache);
11
+
12
+ const report = await diagnose(root, { noCache });
13
+
14
+ if (json) {
15
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
16
+ } else {
17
+ printHumanReport(report);
18
+ }
19
+
20
+ // Exit 0 even when stale — doctor is informational, not a gate.
21
+ // Agents / CI can check report.stale or report.coreUpdateAvailable.
22
+ return 0;
23
+ }
24
+
25
+ /**
26
+ * Diagnose workspace against latest framework state.
27
+ * Result shape:
28
+ * {
29
+ * coreVersion: "0.4.0", // installed
30
+ * coreLatest: "0.5.0" | null, // null on network failure
31
+ * coreUpdateAvailable: boolean,
32
+ * skillsInstalled: ["openpress", ...],
33
+ * skillsLockSource: "quan0715/open-press" | null,
34
+ * pendingMigrations: ["0.5.0"], // versions with docs/migrations notes
35
+ * stale: boolean, // either core or skills behind
36
+ * cachedAt: ISO timestamp
37
+ * }
38
+ */
39
+ export async function diagnose(root, { noCache = false } = {}) {
40
+ const cachePath = path.join(root, ".openpress", "cache", "doctor.json");
41
+
42
+ if (!noCache) {
43
+ const cached = await readCached(cachePath);
44
+ if (cached) return cached;
45
+ }
46
+
47
+ const coreVersion = await readCoreVersion(root);
48
+ const coreLatest = await fetchCoreLatest();
49
+ const skillsInstalled = await listInstalledSkills(root);
50
+ const skillsLockSource = await readSkillsLockSource(root);
51
+ const pendingMigrations = await listPendingMigrations(root, coreVersion, coreLatest);
52
+
53
+ const coreUpdateAvailable = Boolean(
54
+ coreVersion && coreLatest && coreVersion !== coreLatest && semverLt(coreVersion, coreLatest),
55
+ );
56
+
57
+ const report = {
58
+ coreVersion,
59
+ coreLatest,
60
+ coreUpdateAvailable,
61
+ skillsInstalled,
62
+ skillsLockSource,
63
+ pendingMigrations,
64
+ stale: coreUpdateAvailable || pendingMigrations.length > 0,
65
+ cachedAt: new Date().toISOString(),
66
+ };
67
+
68
+ await writeCached(cachePath, report).catch(() => {});
69
+ return report;
70
+ }
71
+
72
+ async function readCached(cachePath) {
73
+ try {
74
+ const stats = await stat(cachePath);
75
+ if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) return null;
76
+ return JSON.parse(await readFile(cachePath, "utf8"));
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ async function writeCached(cachePath, report) {
83
+ await mkdir(path.dirname(cachePath), { recursive: true });
84
+ await writeFile(cachePath, JSON.stringify(report, null, 2) + "\n", "utf8");
85
+ }
86
+
87
+ async function readCoreVersion(root) {
88
+ // Try workspace package.json deps first; fall back to installed package.
89
+ try {
90
+ const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
91
+ const range = pkg.dependencies?.[CORE_PACKAGE] ?? pkg.devDependencies?.[CORE_PACKAGE];
92
+ if (range) {
93
+ // Try the installed version (more accurate than the range).
94
+ try {
95
+ const installed = JSON.parse(
96
+ await readFile(path.join(root, "node_modules", CORE_PACKAGE, "package.json"), "utf8"),
97
+ );
98
+ return installed.version;
99
+ } catch {
100
+ return range.replace(/^[\^~>=<\s]+/, "");
101
+ }
102
+ }
103
+ } catch {}
104
+
105
+ // Self-bundled framework (cli scaffolded workspace): pkg.version is the framework version.
106
+ try {
107
+ const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
108
+ if (pkg.name === CORE_PACKAGE) return pkg.version;
109
+ } catch {}
110
+
111
+ return null;
112
+ }
113
+
114
+ async function fetchCoreLatest() {
115
+ try {
116
+ const res = await fetch(`https://registry.npmjs.org/${CORE_PACKAGE}/latest`, {
117
+ headers: { Accept: "application/json" },
118
+ signal: AbortSignal.timeout(5000),
119
+ });
120
+ if (!res.ok) return null;
121
+ const data = await res.json();
122
+ return typeof data.version === "string" ? data.version : null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ async function listInstalledSkills(root) {
129
+ const skillsDir = path.join(root, ".agents", "skills");
130
+ try {
131
+ const { readdir } = await import("node:fs/promises");
132
+ const entries = await readdir(skillsDir, { withFileTypes: true });
133
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
134
+ } catch {
135
+ return [];
136
+ }
137
+ }
138
+
139
+ async function readSkillsLockSource(root) {
140
+ try {
141
+ const lock = JSON.parse(await readFile(path.join(root, "skills-lock.json"), "utf8"));
142
+ const sources = lock?.sources;
143
+ if (Array.isArray(sources) && sources.length > 0) return sources[0]?.source ?? null;
144
+ return null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ async function listPendingMigrations(root, currentVersion, latestVersion) {
151
+ if (!currentVersion || !latestVersion || !semverLt(currentVersion, latestVersion)) return [];
152
+ // Look for docs/migrations/<version>.md files for versions in (current, latest].
153
+ const migrationsDir = path.join(root, "docs", "migrations");
154
+ try {
155
+ const { readdir } = await import("node:fs/promises");
156
+ const files = await readdir(migrationsDir);
157
+ return files
158
+ .filter((f) => /^\d+\.\d+\.\d+\.md$/.test(f))
159
+ .map((f) => f.replace(/\.md$/, ""))
160
+ .filter((v) => semverGt(v, currentVersion) && !semverGt(v, latestVersion))
161
+ .sort(semverCompare);
162
+ } catch {
163
+ return [];
164
+ }
165
+ }
166
+
167
+ function semverParse(v) {
168
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
169
+ if (!m) return [0, 0, 0];
170
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
171
+ }
172
+ function semverCompare(a, b) {
173
+ const A = semverParse(a);
174
+ const B = semverParse(b);
175
+ for (let i = 0; i < 3; i++) if (A[i] !== B[i]) return A[i] - B[i];
176
+ return 0;
177
+ }
178
+ function semverLt(a, b) { return semverCompare(a, b) < 0; }
179
+ function semverGt(a, b) { return semverCompare(a, b) > 0; }
180
+
181
+ function printHumanReport(report) {
182
+ const lines = [];
183
+ lines.push("○ open-press doctor");
184
+ lines.push("");
185
+ lines.push("framework");
186
+ if (report.coreVersion) {
187
+ if (report.coreLatest === null) {
188
+ lines.push(` ? @open-press/core: ${report.coreVersion} installed (couldn't check latest — offline?)`);
189
+ } else if (report.coreUpdateAvailable) {
190
+ lines.push(` ⚠ @open-press/core: ${report.coreVersion} installed → ${report.coreLatest} available`);
191
+ } else {
192
+ lines.push(` ✓ @open-press/core: ${report.coreVersion} (latest)`);
193
+ }
194
+ } else {
195
+ lines.push(" ? @open-press/core: not detected in this workspace");
196
+ }
197
+ lines.push("");
198
+ lines.push("skills");
199
+ if (report.skillsInstalled.length === 0) {
200
+ lines.push(" ? no skills installed under .agents/skills/");
201
+ lines.push(" run: npx skills add quan0715/open-press");
202
+ } else {
203
+ lines.push(` ✓ ${report.skillsInstalled.length} skills installed`);
204
+ if (report.skillsLockSource) {
205
+ lines.push(` source: ${report.skillsLockSource}`);
206
+ lines.push(" refresh: npx skills upgrade");
207
+ }
208
+ }
209
+ lines.push("");
210
+ lines.push("migrations");
211
+ if (report.pendingMigrations.length === 0) {
212
+ if (report.coreUpdateAvailable) {
213
+ lines.push(` ✓ no breaking migrations documented for the ${report.coreLatest} window`);
214
+ } else {
215
+ lines.push(" ✓ up to date");
216
+ }
217
+ } else {
218
+ lines.push(` ⚠ ${report.pendingMigrations.length} migration note(s) since your version:`);
219
+ for (const v of report.pendingMigrations) lines.push(` - docs/migrations/${v}.md`);
220
+ }
221
+ lines.push("");
222
+ if (report.stale) {
223
+ lines.push("next");
224
+ lines.push(" npx open-press upgrade # apply all updates (agent-driven)");
225
+ lines.push(" npx open-press doctor --json # machine-readable output");
226
+ lines.push("");
227
+ }
228
+ process.stdout.write(lines.join("\n"));
229
+ }
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { buildReactPdf } from "./_shared.mjs";
2
+ import { CLI_ENTRY, STATIC_SERVER, buildReactPdf, formatNodeScriptCommand } from "./_shared.mjs";
3
3
 
4
4
  export async function run({ root, config, options, recurse }) {
5
5
  const outputPath = options.output ? path.resolve(root, options.output) : undefined;
@@ -7,8 +7,8 @@ export async function run({ root, config, options, recurse }) {
7
7
  const relOutput = path.relative(root, outputPath ?? config.paths.pdf);
8
8
  const host = options.host ?? "127.0.0.1";
9
9
  const port = options.port ?? "5185";
10
- console.log("Command: node engine/cli.mjs render . --renderer react");
11
- console.log(`Command: node engine/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
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
12
  console.log(`Command: Chrome --print-to-pdf=${relOutput} http://${host}:${port}/?print=1`);
13
13
  return 0;
14
14
  }
@@ -1,4 +1,4 @@
1
- import { runCommand } from "./_shared.mjs";
1
+ import { CLI_ENTRY, STATIC_SERVER, formatNodeScriptCommand, runCommand } from "./_shared.mjs";
2
2
 
3
3
  export async function run({ root, config, options, recurse }) {
4
4
  const renderer = options.renderer ?? "react";
@@ -12,9 +12,9 @@ export async function run({ root, config, options, recurse }) {
12
12
  if (options.dryRun) {
13
13
  console.log(`OpenPress preview URL: ${url}`);
14
14
  if (!options.noBuild) {
15
- console.log("Command: node engine/cli.mjs render . --renderer react");
15
+ console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
16
16
  }
17
- console.log(`Command: node engine/static-server.mjs ${config.outputDir} --host ${host} --port ${port} --workspace .`);
17
+ console.log(`Command: ${formatNodeScriptCommand(root, STATIC_SERVER)} ${config.outputDir} --host ${host} --port ${port} --workspace .`);
18
18
  return 0;
19
19
  }
20
20
  if (!options.noBuild) {
@@ -22,5 +22,5 @@ export async function run({ root, config, options, recurse }) {
22
22
  if (renderCode !== 0) return renderCode;
23
23
  }
24
24
  console.log(`OpenPress preview: ${url}`);
25
- return runCommand("node", ["engine/static-server.mjs", config.outputDir, "--host", host, "--port", port, "--workspace", "."], root);
25
+ return runCommand("node", [STATIC_SERVER, config.outputDir, "--host", host, "--port", port, "--workspace", "."], root);
26
26
  }