@open-press/core 0.3.0 → 0.6.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.
- package/engine/cli.mjs +6 -0
- package/engine/commands/_shared.mjs +9 -2
- package/engine/commands/deploy.mjs +3 -3
- package/engine/commands/dev.mjs +25 -2
- package/engine/commands/doctor.mjs +229 -0
- package/engine/commands/pdf.mjs +3 -3
- package/engine/commands/preview.mjs +4 -4
- package/engine/commands/upgrade.mjs +117 -0
- package/package.json +3 -1
- package/vite.config.ts +26 -11
package/engine/cli.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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:
|
|
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;
|
package/engine/commands/dev.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { exportDocument } from "../document-export.mjs";
|
|
2
|
-
import {
|
|
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(
|
|
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
|
+
}
|
package/engine/commands/pdf.mjs
CHANGED
|
@@ -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(
|
|
11
|
-
console.log(`Command:
|
|
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(
|
|
15
|
+
console.log(`Command: ${formatNodeScriptCommand(root, CLI_ENTRY)} render . --renderer react`);
|
|
16
16
|
}
|
|
17
|
-
console.log(`Command:
|
|
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", [
|
|
25
|
+
return runCommand("node", [STATIC_SERVER, config.outputDir, "--host", host, "--port", port, "--workspace", "."], root);
|
|
26
26
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { diagnose } from "./doctor.mjs";
|
|
5
|
+
import { runCommand } from "./_shared.mjs";
|
|
6
|
+
|
|
7
|
+
export async function run({ root, options }) {
|
|
8
|
+
const dryRun = Boolean(options?.dryRun);
|
|
9
|
+
const skipSkills = Boolean(options?.noSkills);
|
|
10
|
+
const skipDeps = Boolean(options?.noDeps);
|
|
11
|
+
const json = Boolean(options?.json);
|
|
12
|
+
|
|
13
|
+
// 1. Fresh diagnose (force re-check, ignore cache).
|
|
14
|
+
const before = await diagnose(root, { noCache: true });
|
|
15
|
+
|
|
16
|
+
if (!before.stale) {
|
|
17
|
+
const message = "open-press is already up to date.";
|
|
18
|
+
if (json) {
|
|
19
|
+
process.stdout.write(JSON.stringify({ status: "noop", before }, null, 2) + "\n");
|
|
20
|
+
} else {
|
|
21
|
+
process.stdout.write(`✓ ${message}\n`);
|
|
22
|
+
}
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!json) {
|
|
27
|
+
process.stdout.write("○ open-press upgrade\n\n");
|
|
28
|
+
if (before.coreUpdateAvailable) {
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
` @open-press/core: ${before.coreVersion} → ${before.coreLatest}\n`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (before.pendingMigrations.length > 0) {
|
|
34
|
+
process.stdout.write(` migration notes: ${before.pendingMigrations.join(", ")}\n`);
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (dryRun) {
|
|
40
|
+
if (!json) {
|
|
41
|
+
process.stdout.write("dry run — nothing changed. The agent should:\n");
|
|
42
|
+
process.stdout.write(" 1. read each docs/migrations/<version>.md for document-level changes\n");
|
|
43
|
+
process.stdout.write(" 2. apply edits to document/ where needed\n");
|
|
44
|
+
process.stdout.write(" 3. re-run: npx open-press upgrade (without --dry-run)\n");
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write(JSON.stringify({ status: "dry-run", before }, null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Refresh framework dep (only when workspace declares @open-press/core).
|
|
52
|
+
if (!skipDeps && (await hasCoreDep(root))) {
|
|
53
|
+
if (!json) process.stdout.write("▸ updating @open-press/core via npm…\n");
|
|
54
|
+
const code = runCommand("npm", ["update", "@open-press/core"], root);
|
|
55
|
+
if (code !== 0) {
|
|
56
|
+
if (!json) process.stdout.write(" ⚠ npm update returned non-zero; continuing\n");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Refresh skills (npx skills upgrade respects skills-lock.json).
|
|
61
|
+
if (!skipSkills) {
|
|
62
|
+
if (!json) process.stdout.write("▸ refreshing skills via npx skills upgrade…\n");
|
|
63
|
+
runCommand("npx", ["-y", "skills@latest", "upgrade"], root);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4. Surface migration notes for the agent to read.
|
|
67
|
+
const migrationContents = await loadMigrations(root, before.pendingMigrations);
|
|
68
|
+
|
|
69
|
+
// 5. Re-diagnose to confirm the move.
|
|
70
|
+
const after = await diagnose(root, { noCache: true });
|
|
71
|
+
|
|
72
|
+
if (json) {
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
JSON.stringify(
|
|
75
|
+
{ status: "applied", before, after, migrationContents: migrationContents.map((m) => m.path) },
|
|
76
|
+
null,
|
|
77
|
+
2,
|
|
78
|
+
) + "\n",
|
|
79
|
+
);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.stdout.write("\n✓ upgrade applied. Now read these migration notes:\n\n");
|
|
84
|
+
if (migrationContents.length === 0) {
|
|
85
|
+
process.stdout.write(" (no migration docs in this version range)\n\n");
|
|
86
|
+
} else {
|
|
87
|
+
for (const m of migrationContents) {
|
|
88
|
+
process.stdout.write(` ─ ${m.path}\n`);
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write(
|
|
91
|
+
"\nAgent: open each file, identify document-level changes, grep document/ for affected patterns, propose edits before applying.\n",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
process.stdout.write("\nVerify with:\n npm run openpress:validate\n npm run openpress:render\n\n");
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function hasCoreDep(root) {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
102
|
+
return Boolean(pkg.dependencies?.["@open-press/core"] || pkg.devDependencies?.["@open-press/core"]);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function loadMigrations(root, versions) {
|
|
109
|
+
const results = [];
|
|
110
|
+
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) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-press/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "open-press core — runtime primitives, CLI, and render pipeline for AI-first fixed-layout documents.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"js-yaml": "^4.1.1",
|
|
49
49
|
"katex": "^0.16.47",
|
|
50
50
|
"lucide-react": "^1.16.0",
|
|
51
|
+
"playwright": "^1.60.0",
|
|
52
|
+
"postcss": "^8.5.6",
|
|
51
53
|
"react": "^19.2.6",
|
|
52
54
|
"react-dom": "^19.2.6",
|
|
53
55
|
"rehype-katex": "^7.0.1",
|
package/vite.config.ts
CHANGED
|
@@ -9,13 +9,18 @@ import { loadConfig, publicPdfHref } from "./engine/config.mjs";
|
|
|
9
9
|
import { handleCommentRequest } from "./engine/react/comment-endpoint.mjs";
|
|
10
10
|
import { handleProjectAssetRequest } from "./engine/react/project-asset-endpoint.mjs";
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const workspaceRoot =
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const frameworkRoot = fileURLToPath(new URL("./", import.meta.url));
|
|
13
|
+
const workspaceRoot = process.env.OPENPRESS_WORKSPACE_ROOT
|
|
14
|
+
? path.resolve(process.env.OPENPRESS_WORKSPACE_ROOT)
|
|
15
|
+
: frameworkRoot;
|
|
16
|
+
const sourceRoot = path.join(frameworkRoot, "src");
|
|
17
|
+
const openpressCliPath = path.join(frameworkRoot, "engine", "cli.mjs");
|
|
18
|
+
const staticServerPath = path.join(frameworkRoot, "engine", "static-server.mjs");
|
|
19
|
+
const openpressCoreEntry = path.join(frameworkRoot, "src", "openpress", "core", "index.tsx");
|
|
16
20
|
const openpressConfig = await loadConfig(workspaceRoot);
|
|
17
21
|
const outputDir = openpressConfig.paths.outputDir;
|
|
18
|
-
const reactDocumentRoot =
|
|
22
|
+
const reactDocumentRoot = openpressConfig.paths.documentRoot;
|
|
23
|
+
const reactDocumentComponentsRoot = openpressConfig.paths.componentsDir;
|
|
19
24
|
const reactDocumentEntry = path.join(reactDocumentRoot, "index.tsx");
|
|
20
25
|
const activeContentDir = await fileExists(reactDocumentEntry)
|
|
21
26
|
? path.join(reactDocumentRoot, "chapters")
|
|
@@ -48,6 +53,7 @@ export default defineConfig({
|
|
|
48
53
|
plugins: [openpressLocalDeployPlugin(), react()],
|
|
49
54
|
define: workspaceDefines,
|
|
50
55
|
resolve: {
|
|
56
|
+
dedupe: ["react", "react-dom", "@mdx-js/react"],
|
|
51
57
|
alias: {
|
|
52
58
|
"@openpress/core": openpressCoreEntry,
|
|
53
59
|
"@/components": reactDocumentComponentsRoot,
|
|
@@ -69,6 +75,9 @@ export default defineConfig({
|
|
|
69
75
|
server: {
|
|
70
76
|
host: "127.0.0.1",
|
|
71
77
|
port: 5173,
|
|
78
|
+
fs: {
|
|
79
|
+
allow: Array.from(new Set([frameworkRoot, workspaceRoot])),
|
|
80
|
+
},
|
|
72
81
|
watch: {
|
|
73
82
|
ignored: ["**/.openpress/tmp/**", `**/${openpressConfig.outputDir}/**`],
|
|
74
83
|
},
|
|
@@ -201,7 +210,7 @@ async function handleLocalPdfExportRequest(req: IncomingMessage, res: ServerResp
|
|
|
201
210
|
ok: result.code === 0 && exists,
|
|
202
211
|
code: result.code,
|
|
203
212
|
pdf: `/__openpress/local-pdf-file?ts=${Date.now()}`,
|
|
204
|
-
command: "
|
|
213
|
+
command: openpressCliCommand(["pdf", "."]),
|
|
205
214
|
stdout: result.stdout,
|
|
206
215
|
stderr: result.stderr,
|
|
207
216
|
});
|
|
@@ -266,7 +275,7 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
|
|
|
266
275
|
deploy_adapter: openpressConfig.deploy.adapter,
|
|
267
276
|
deploy_source: openpressConfig.deploy.source,
|
|
268
277
|
deploy_project_name: openpressConfig.deploy.projectName,
|
|
269
|
-
command: "
|
|
278
|
+
command: openpressCliCommand(["deploy", ".", "--confirm"]),
|
|
270
279
|
});
|
|
271
280
|
return;
|
|
272
281
|
}
|
|
@@ -285,7 +294,7 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
|
|
|
285
294
|
pdf: deployedUrl ? `${deployedUrl}/${openpressConfig.pdf.filename}` : deploymentInfo.pdf,
|
|
286
295
|
public_url: publicUrl,
|
|
287
296
|
dirty: false,
|
|
288
|
-
command: "
|
|
297
|
+
command: openpressCliCommand(["deploy", ".", "--confirm"]),
|
|
289
298
|
stdout: result.stdout,
|
|
290
299
|
stderr: result.stderr,
|
|
291
300
|
});
|
|
@@ -293,7 +302,7 @@ async function handleLocalDeployRequest(req: IncomingMessage, res: ServerRespons
|
|
|
293
302
|
|
|
294
303
|
function runLocalPdfExport() {
|
|
295
304
|
return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
|
296
|
-
const child = spawn("node", [
|
|
305
|
+
const child = spawn("node", [openpressCliPath, "pdf", "."], {
|
|
297
306
|
cwd: workspaceRoot,
|
|
298
307
|
shell: false,
|
|
299
308
|
});
|
|
@@ -316,7 +325,7 @@ function runLocalPdfExport() {
|
|
|
316
325
|
|
|
317
326
|
function runLocalDeploy() {
|
|
318
327
|
return new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
|
319
|
-
const child = spawn("node", [
|
|
328
|
+
const child = spawn("node", [openpressCliPath, "deploy", ".", "--confirm"], {
|
|
320
329
|
cwd: workspaceRoot,
|
|
321
330
|
shell: false,
|
|
322
331
|
});
|
|
@@ -405,7 +414,7 @@ function getLocalDeploymentSourcePaths() {
|
|
|
405
414
|
openpressConfig.paths.themeDir,
|
|
406
415
|
openpressConfig.paths.designDoc,
|
|
407
416
|
openpressConfig.paths.componentsDir,
|
|
408
|
-
path.join(
|
|
417
|
+
path.join(frameworkRoot, "src"),
|
|
409
418
|
path.join(workspaceRoot, "index.html"),
|
|
410
419
|
path.join(workspaceRoot, "package.json"),
|
|
411
420
|
path.join(workspaceRoot, "openpress.config.mjs"),
|
|
@@ -414,6 +423,12 @@ function getLocalDeploymentSourcePaths() {
|
|
|
414
423
|
];
|
|
415
424
|
}
|
|
416
425
|
|
|
426
|
+
function openpressCliCommand(args: string[]) {
|
|
427
|
+
const relativeCliPath = path.relative(workspaceRoot, openpressCliPath).replaceAll("\\", "/");
|
|
428
|
+
const displayCliPath = relativeCliPath && !relativeCliPath.startsWith("../") ? relativeCliPath : openpressCliPath;
|
|
429
|
+
return `node ${displayCliPath} ${args.join(" ")}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
417
432
|
async function findNewestLocalSourceMtime(paths: string[]) {
|
|
418
433
|
const times = await Promise.all(paths.map((sourcePath) => findNewestLocalMtime(sourcePath)));
|
|
419
434
|
return Math.max(0, ...times);
|