@oj-module/cli 0.2.57 → 0.2.63
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/devkit/SKILL.md +1 -0
- package/devkit/manual.md +19 -1
- package/devkit/module-dev-and-cdn-playground.md +1 -0
- package/package.json +2 -2
- package/shell-dist/assets/{index-CImwskAz.js → index-BKtXxIXz.js} +32 -2
- package/shell-dist/assets/runtime.js +3 -3
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +155 -110
- package/src/__tests__/port-check.test.ts +40 -0
- package/src/__tests__/vendor.test.ts +39 -0
- package/src/build/backend.ts +20 -4
- package/src/build/module-def.ts +20 -1
- package/src/cli.ts +7 -0
- package/src/contract/emit-stub.ts +7 -3
- package/src/dev-secrets.ts +315 -0
- package/src/dev.ts +26 -3
- package/src/init.ts +15 -4
- package/src/lifecycle/__tests__/bundle.test.ts +18 -1
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +19 -2
- package/src/market/bootstrap.ts +5 -2
- package/src/new.ts +27 -0
- package/src/oj.ts +82 -9
- package/src/port-check.ts +38 -0
- package/src/rekey.ts +51 -0
- package/src/static-handler.ts +7 -2
- package/src/usage.ts +12 -2
- package/src/vendor.ts +32 -5
- package/vendor/host-versions.json +3 -3
package/src/new.ts
CHANGED
|
@@ -26,6 +26,26 @@ import { readBuiltinModules } from "./builtin";
|
|
|
26
26
|
import { tryResolveLayout } from "./layout";
|
|
27
27
|
import { assertValidSolutionName, RESERVED_NAMES } from "./manifest-contract";
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 内建聚合模块 app 的 web 路由根(src/app/web 下各 routes.ts 的顶层 path):
|
|
31
|
+
* 这些路径由框架内置页面独占。ojm new 生成的默认路由为 /name,
|
|
32
|
+
* 若 name 命中其中之一,会与内置页面撞 route id,导致运行时白屏。
|
|
33
|
+
* 这里在脚手架阶段即拒绝,避免生成即炸。
|
|
34
|
+
*
|
|
35
|
+
* 与 readBuiltinModules(内建模块名)互补:内建模块名只覆盖 home /
|
|
36
|
+
* personal-center / system,而 login / change-password / notification-center 是
|
|
37
|
+
* app 聚合内的路由根,并非独立内建模块名,故单列此表。
|
|
38
|
+
* app 路由根增删时此处须同步。
|
|
39
|
+
*/
|
|
40
|
+
const BUILTIN_ROUTE_ROOTS = new Set([
|
|
41
|
+
"home",
|
|
42
|
+
"login",
|
|
43
|
+
"change-password",
|
|
44
|
+
"notification-center",
|
|
45
|
+
"personal-center",
|
|
46
|
+
"system",
|
|
47
|
+
]);
|
|
48
|
+
|
|
29
49
|
export interface NewSolutionOptions {
|
|
30
50
|
/** 示例路由布局(默认 container;none = 无外壳裸页面) */
|
|
31
51
|
layout?: "container" | "none"
|
|
@@ -76,6 +96,13 @@ export function createSolution(projectRoot: string, name: string, opts: NewSolut
|
|
|
76
96
|
+ "想改造自带模块:`ojm new <新名>` 生成骨架后把代码复制过去(自带身份按目录名判定,复制即脱离)。",
|
|
77
97
|
);
|
|
78
98
|
}
|
|
99
|
+
if (BUILTIN_ROUTE_ROOTS.has(name)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`[ojm] 「${name}」与框架内置页面路由根(${[...BUILTIN_ROUTE_ROOTS].join("/")})撞车——`
|
|
102
|
+
+ `ojm new 生成的默认路由 /${name} 会与内置 ${name} 页面冲突(route id 碰撞白屏)。`
|
|
103
|
+
+ "请换一个不与内置页面撞名的方案名。",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
79
106
|
const dir = path.join(projectRoot, "src", name);
|
|
80
107
|
if (fs.existsSync(dir))
|
|
81
108
|
throw new Error(`[ojm] src/${name}/ 已存在。换个名字,或删掉该目录后重试。`);
|
package/src/oj.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* 由 `ojm vendor` 的最低版本门禁挡住(见 src/vendor.ts 的 MIN_OJ_VERSION)。
|
|
10
10
|
*
|
|
11
11
|
* 健康判据:GET {base}/health 状态码 <400(oj 内置匿名健康端点;2xx/3xx 均视为就绪)。
|
|
12
|
-
* 秒退:ready
|
|
12
|
+
* 秒退:ready 拒绝,错误信息优先带 stderr 尾部(证书缺失等常见错因可直接读出);
|
|
13
|
+
* 若 oj 把 FATAL 落盘到 logs/ 而未进 stderr(手册说 `--console-log` 会镜像到终端,但
|
|
14
|
+
* 部分环境仍可能只落盘),再读 `<configDir>/logs/server-*.log` 最新文件兜底,绝不输出
|
|
15
|
+
* 无意义的 "(无输出)"(方案 A' 失败提示不吞,评审 E-1)。
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import type { Buffer } from "node:buffer";
|
|
@@ -21,6 +24,43 @@ import process from "node:process";
|
|
|
21
24
|
import { readOjPluginsDir, readOjPort } from "./oj-config";
|
|
22
25
|
import { resolveOjBin } from "./vendor";
|
|
23
26
|
|
|
27
|
+
/**
|
|
28
|
+
* logs/ 兜底(评审 E-1):oj 启动期 FATAL 有时只落盘不进 stderr。
|
|
29
|
+
* 给定 oj 的日志目录(通常 `<configDir>/logs`),挑最新的 `server-*.log`,
|
|
30
|
+
* 抽出 ERROR/FATAL/panic 等关键行(没有则取末 30 行),供失败信息兜底展示。
|
|
31
|
+
* 找不到日志目录 / 文件即返回空串(调用方维持 "(无输出)")。
|
|
32
|
+
*/
|
|
33
|
+
function readLatestLogError(logsDir: string): string {
|
|
34
|
+
if (!logsDir || !fs.existsSync(logsDir))
|
|
35
|
+
return "";
|
|
36
|
+
let entries: fs.Dirent[];
|
|
37
|
+
try {
|
|
38
|
+
entries = fs.readdirSync(logsDir, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
const logs = entries
|
|
44
|
+
.filter(e => e.isFile() && /^server-.*\.log$/.test(e.name))
|
|
45
|
+
.map((e) => {
|
|
46
|
+
try {
|
|
47
|
+
return { name: e.name, mtime: fs.statSync(path.join(logsDir, e.name)).mtimeMs };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.filter((x): x is { name: string, mtime: number } => x !== null)
|
|
54
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
55
|
+
if (logs.length === 0)
|
|
56
|
+
return "";
|
|
57
|
+
const text = fs.readFileSync(path.join(logsDir, logs[0].name), "utf-8");
|
|
58
|
+
const lines = text.split(/\r?\n/);
|
|
59
|
+
const keyLines = lines.filter(l => /error|fatal|panic|denied|invalid|unable|cannot|fail|exception|refused/i.test(l));
|
|
60
|
+
const picked = keyLines.length > 0 ? keyLines.slice(-30) : lines.slice(-30);
|
|
61
|
+
return picked.join("\n").trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
24
64
|
export interface OjProcess {
|
|
25
65
|
port: number
|
|
26
66
|
/** 健康就绪(或失败拒绝:秒退带 stderr 尾部 / 超时) */
|
|
@@ -63,8 +103,12 @@ export function startOj(
|
|
|
63
103
|
apiSource: string | string[],
|
|
64
104
|
poll: StartOjPoll = {},
|
|
65
105
|
extraArgs: string[] = [],
|
|
106
|
+
logsDir?: string,
|
|
66
107
|
): OjProcess {
|
|
67
108
|
const port = readOjPort(configPath);
|
|
109
|
+
// logs/ 兜底目录(评审 E-1):oj FATAL 有时只落盘不进 stderr。缺省按 config 同级
|
|
110
|
+
// 的 logs/(与 oj 默认 logs_dir 一致);调用方可显式覆盖(preview 派生 config 等)。
|
|
111
|
+
const resolvedLogsDir = logsDir ?? path.join(path.dirname(configPath), "logs");
|
|
68
112
|
// 约定:vendor 二进制在 <project>/bin/oj。工程根不能按「config 往上两级」算:
|
|
69
113
|
// module-root 布局的 config 在仓库根(<project>/config.yaml),往上两级会越过工程,
|
|
70
114
|
// 落到父目录找 bin/oj。改为「向上找最近的含 bin/ 的目录」,两种布局都成立。
|
|
@@ -84,10 +128,27 @@ export function startOj(
|
|
|
84
128
|
// 注入条件:用户未在 toml 显式声明 plugins_dir(env 优先级更高,注入会覆盖),
|
|
85
129
|
// 且 bin/plugins 确实存在(缺失时保持 oj 原行为,不生次生错误)。
|
|
86
130
|
const pluginsDir = path.join(projectRoot, "bin", "plugins");
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
131
|
+
// win32:oj 是原生 Windows 二进制,从 Git Bash / MSYS 终端启动时继承 MSYS 风格 PATH
|
|
132
|
+
// (如 `/c/WINDOWS/system32`),Windows CreateProcess 无法按该形式解析可执行文件 → 内部
|
|
133
|
+
// `tar` 等系统工具 "command not found in PATH"(市场安装解包即挂)。追加原生 Windows 系统
|
|
134
|
+
// 目录(tar.exe 所在);cmd/PowerShell 用户 PATH 本就含这些,无副作用。
|
|
135
|
+
// win32: Git Bash/MSYS 把 PATH 写成 /usr/bin 这类 Unix 风格条目,oj 内部按 Windows 规则检索命令
|
|
136
|
+
// 时无法据此定位 tar.exe(系统 tar 在 C:\Windows\System32),市场安装解包会报 tar not found in PATH。
|
|
137
|
+
// 剔除无盘符的 Unix 风格条目、仅保留 Windows 绝对路径,并兜底补上系统目录与 Git bin。
|
|
138
|
+
// cmd/PowerShell 用户 PATH 本就全是 Windows 形态,过滤无副作用。
|
|
139
|
+
const env: NodeJS.ProcessEnv | undefined = (() => {
|
|
140
|
+
let base: NodeJS.ProcessEnv = process.env;
|
|
141
|
+
if (process.platform === "win32") {
|
|
142
|
+
const cur = process.env.PATH ?? process.env.Path ?? "";
|
|
143
|
+
const winEntries = cur.split(";").filter(e => /^[A-Z]:[\\/]/i.test(e.trim()));
|
|
144
|
+
const sysDirs = ["C:\\Windows\\System32", "C:\\Windows", "C:\\Program Files\\Git\\usr\\bin"];
|
|
145
|
+
const missing = sysDirs.filter(d => !winEntries.some(e => e.toLowerCase().startsWith(d.toLowerCase())));
|
|
146
|
+
base = { ...process.env, PATH: [...winEntries, ...missing].join(";") };
|
|
147
|
+
}
|
|
148
|
+
return !readOjPluginsDir(configPath) && fs.existsSync(pluginsDir)
|
|
149
|
+
? { ...base, OJ_PLUGINS_DIR: pluginsDir }
|
|
150
|
+
: base;
|
|
151
|
+
})();
|
|
91
152
|
|
|
92
153
|
let stderrTail = "";
|
|
93
154
|
// --console-log:oj 新版终端默认静默(console_log 缺省 false,只落 logs/),
|
|
@@ -109,6 +170,13 @@ export function startOj(
|
|
|
109
170
|
|
|
110
171
|
let settled = false;
|
|
111
172
|
let pollTimer: NodeJS.Timeout | undefined;
|
|
173
|
+
// oj 端口被占用时 stderr 通常会带这些签名(Rust 绑定失败 / 系统错误码);命中即
|
|
174
|
+
// 补一条「先杀占用进程」的可操作提示,避免只甩晦涩的 oj 原生报错。
|
|
175
|
+
const portInUseHint = (text: string): string => {
|
|
176
|
+
if (/Address already in use|EADDRINUSE|os error 98|os error 10048|地址已被使用|端口已被占用/i.test(text))
|
|
177
|
+
return "\n可能原因:后端端口被占用(通常是上次 ojm dev 未正常退出留下的 oj 孤儿进程)。请先结束占用进程再重试。";
|
|
178
|
+
return "";
|
|
179
|
+
};
|
|
112
180
|
const ready = new Promise<void>((resolve, reject) => {
|
|
113
181
|
const intervalMs = poll.intervalMs ?? 500;
|
|
114
182
|
const timeoutMs = poll.timeoutMs ?? 10_000;
|
|
@@ -129,9 +197,12 @@ export function startOj(
|
|
|
129
197
|
}, intervalMs);
|
|
130
198
|
pollTimer.unref();
|
|
131
199
|
child.once("exit", (code) => {
|
|
200
|
+
const hint = portInUseHint(stderrTail);
|
|
201
|
+
// A' 失败提示不吞:stderr 空时读 logs/ 最新 server-*.log 兜底(评审 E-1)
|
|
202
|
+
const tail = stderrTail.trim() || readLatestLogError(resolvedLogsDir);
|
|
132
203
|
finish(new Error(
|
|
133
|
-
`[ojm] oj 进程异常退出(code ${code}
|
|
134
|
-
+ `stderr 尾部:\n${
|
|
204
|
+
`[ojm] oj 进程异常退出(code ${code})。${hint}\n`
|
|
205
|
+
+ `stderr 尾部:\n${tail || "(无输出)"}`,
|
|
135
206
|
));
|
|
136
207
|
});
|
|
137
208
|
// spawn 本身失败(如 ENOENT)只发 error 不发 exit,必须单独兜住
|
|
@@ -141,9 +212,11 @@ export function startOj(
|
|
|
141
212
|
finish(new Error(`[ojm] 无法启动 oj(${binPath}):${err.message}${hint}`));
|
|
142
213
|
});
|
|
143
214
|
const timeout = setTimeout(() => {
|
|
215
|
+
const hint = portInUseHint(stderrTail);
|
|
216
|
+
const tail = stderrTail.trim() || readLatestLogError(resolvedLogsDir);
|
|
144
217
|
finish(new Error(
|
|
145
|
-
`[ojm] oj 健康检查超时(${Math.round(timeoutMs / 1000)}s):${healthUrl}
|
|
146
|
-
+ `stderr 尾部:\n${
|
|
218
|
+
`[ojm] oj 健康检查超时(${Math.round(timeoutMs / 1000)}s):${healthUrl} 一直不可达。${hint}\n`
|
|
219
|
+
+ `stderr 尾部:\n${tail || "(无输出)"}`,
|
|
147
220
|
));
|
|
148
221
|
}, timeoutMs);
|
|
149
222
|
timeout.unref();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 探测某端口是否已被占用(即有进程在监听)。
|
|
5
|
+
*
|
|
6
|
+
* 用于 `ojm dev` 启动前的后端端口预检:若 oj 的目标端口已被(通常是上次未正常
|
|
7
|
+
* 退出、遗留的)oj 孤儿进程占据,提前给出清晰提示并禁止启动,避免 oj 健康检查
|
|
8
|
+
* 超时后才以晦涩信息失败。
|
|
9
|
+
*
|
|
10
|
+
* 探测方式:本进程尝试在该端口上 `listen`——
|
|
11
|
+
* - `EADDRINUSE` → 端口已被占用,返回 true;
|
|
12
|
+
* - 绑定成功 → 端口空闲,立即释放并返回 false;
|
|
13
|
+
* - 其它错误(如权限)→ 无法确认占用,保守返回 false(宁可放行,不让带病禁止)。
|
|
14
|
+
*
|
|
15
|
+
* 不用「TCP connect 探活」:某些环境下连向空闲端口的连接会被静默丢弃(既不 RST 也
|
|
16
|
+
* 不 SYN-ACK),导致探测挂起并以「保守判占用」告终,从而误杀每一次 ojm dev 启动。
|
|
17
|
+
* bind 探测同步返回,绝不挂起。
|
|
18
|
+
*/
|
|
19
|
+
export function isPortOccupied(port: number, host = "127.0.0.1"): Promise<boolean> {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const probe = net.createServer();
|
|
22
|
+
let settled = false;
|
|
23
|
+
const done = (occupied: boolean) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
resolve(occupied);
|
|
28
|
+
};
|
|
29
|
+
probe.once("error", (err: NodeJS.ErrnoException) => {
|
|
30
|
+
// EADDRINUSE = 端口确被占用;其余(EACCES 等)无法确认,保守放行
|
|
31
|
+
done(err.code === "EADDRINUSE");
|
|
32
|
+
});
|
|
33
|
+
probe.listen(port, host, () => {
|
|
34
|
+
// 能绑定说明空闲:立即释放后返回 false
|
|
35
|
+
probe.close(() => done(false));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/rekey.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { DevSecretsDeps } from "./dev-secrets";
|
|
2
|
+
/**
|
|
3
|
+
* `ojm rekey` —— 重新生成开发态本机密钥并重封 dev jwt_secret(方案 A' 第 8 点)。
|
|
4
|
+
*
|
|
5
|
+
* 只动 gitignored 的 `config/secrets-*.pem` 与 `config.local.yaml` 里的 dev 覆盖,
|
|
6
|
+
* **绝不**触碰 tracked `config.yaml`(prod 路径零改动,A-3 不变量)。
|
|
7
|
+
*
|
|
8
|
+
* 典型场景:
|
|
9
|
+
* - 本机 dev 密钥对被人误删/改坏,`ojm dev` 因 jws/ENC 解不开起不来 → `ojm rekey` 重生。
|
|
10
|
+
* - 想让本机 jwt_secret 轮换(dev 可随意轮换,无外部密文依赖)。
|
|
11
|
+
* 之后 `ojm dev` 会用新密钥自动重合并重起;无需手改任何 tracked 文件。
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { provisionDevSecrets, resolveLocalConfigPath } from "./dev-secrets";
|
|
16
|
+
import { resolveCertDir, resolveConfigPath } from "./layout";
|
|
17
|
+
|
|
18
|
+
export interface RekeyResult {
|
|
19
|
+
/** 重生后的 `config.local.yaml` 路径 */
|
|
20
|
+
localPath: string
|
|
21
|
+
/** 是否真正重新生成了密钥对(已存在且可用则 rekey 只重封 jwt,不重生成) */
|
|
22
|
+
regeneratedKey: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RekeyDeps extends DevSecretsDeps {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 重新供给开发态本机密钥:删除旧 `secrets-*.pem` → `provisionDevSecrets` 因缺失而重生成,
|
|
29
|
+
* 并跟着重封 jwt_secret 写入 `config.local.yaml`。返回落点信息。
|
|
30
|
+
*/
|
|
31
|
+
export function rekeyCommand(projectRoot: string, deps: RekeyDeps = {}): RekeyResult {
|
|
32
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
33
|
+
if (!fs.existsSync(configPath))
|
|
34
|
+
throw new Error(`[ojm] 未找到 config.yaml(${configPath}),无法 rekey。请确认当前目录是 ojm 工程。`);
|
|
35
|
+
|
|
36
|
+
const keysDir = resolveCertDir(projectRoot);
|
|
37
|
+
const privateKey = path.join(keysDir, "secrets-private.pem");
|
|
38
|
+
const publicKey = path.join(keysDir, "secrets-public.pem");
|
|
39
|
+
const hadKey = fs.existsSync(privateKey) || fs.existsSync(publicKey);
|
|
40
|
+
// 强制重生:先删旧的,provisionDevSecrets 见缺失即 keygen(A' 自动 rekey 的同一入口)
|
|
41
|
+
for (const f of [privateKey, publicKey]) {
|
|
42
|
+
if (fs.existsSync(f))
|
|
43
|
+
fs.rmSync(f);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
provisionDevSecrets(projectRoot, configPath, deps);
|
|
47
|
+
return {
|
|
48
|
+
localPath: resolveLocalConfigPath(configPath),
|
|
49
|
+
regeneratedKey: hadKey,
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/static-handler.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type http from "node:http";
|
|
17
17
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
18
|
-
import { extname, normalize, resolve } from "node:path";
|
|
18
|
+
import { extname, normalize, resolve, sep } from "node:path";
|
|
19
19
|
|
|
20
20
|
const MIME: Record<string, string> = {
|
|
21
21
|
".html": "text/html; charset=utf-8",
|
|
@@ -185,7 +185,12 @@ export function createStaticHandler(opts: StaticHandlerOptions): (req: http.Inco
|
|
|
185
185
|
res.end("400 Bad Request: malformed URL encoding");
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
|
-
|
|
188
|
+
// URL 路径是「类 Unix 斜杠语义」,不能用 path.normalize 的 OS 方言:
|
|
189
|
+
// 在 Windows 上 path.normalize("/assets/x.js") 会变成 "\\assets\\x.js",
|
|
190
|
+
// 既让下方 /__ojm_reload.js 等精确匹配全部落空(404),又让 resolveFile
|
|
191
|
+
// 把前导反斜杠当成「盘符根」解析到 C:\\assets\\...(错位 404)。统一转回
|
|
192
|
+
// 正斜杠——POSIX 上 path.sep 即 "/" 此换行为 no-op,跨平台安全。
|
|
193
|
+
const rel = normalize(urlPath).split(sep).join("/").replace(/^(\.\.[/\\])+/, "");
|
|
189
194
|
|
|
190
195
|
if (opts.reload) {
|
|
191
196
|
if (rel === "/__ojm_reload") {
|
package/src/usage.ts
CHANGED
|
@@ -111,8 +111,18 @@ export const HELP_GROUPS: HelpGroup[] = [
|
|
|
111
111
|
zh: "启动开发服务器(/api 反代 oj + 模块重建 + SSE 刷新)",
|
|
112
112
|
en: "Start dev server (/api proxy to oj + module rebuild + SSE refresh)",
|
|
113
113
|
detail: {
|
|
114
|
-
zh: "ojm dev [port]\n 开发模式快捷键(仅 TTY 终端,类 Vite):\n r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开\n q 退出(后端 oj 一并终止)\n port 缺省 5174(oj 后端端口以 config.yaml 的 server.port
|
|
115
|
-
en: "ojm dev [port]\n Dev-mode shortcuts (TTY only, Vite-like):\n r force rebuild + hot reload h show help c clear screen o open browser\n q quit (terminates the oj backend as well)\n port defaults to 5174 (the oj backend port comes from config.yaml server.port)",
|
|
114
|
+
zh: "ojm dev [port]\n 开发模式快捷键(仅 TTY 终端,类 Vite):\n r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开\n q 退出(后端 oj 一并终止)\n port 缺省 5174(oj 后端端口以 config.yaml 的 server.port 为准)\n 首次 clone 无本机密钥时自动供给 dev 密钥 + 随机 jwt_secret(方案 A'),并合并 config.local.yaml 后启动",
|
|
115
|
+
en: "ojm dev [port]\n Dev-mode shortcuts (TTY only, Vite-like):\n r force rebuild + hot reload h show help c clear screen o open browser\n q quit (terminates the oj backend as well)\n port defaults to 5174 (the oj backend port comes from config.yaml server.port)\n On first clone without local keys, dev auto-provisions a dev keypair + random jwt_secret (plan A') and starts after merging config.local.yaml",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "rekey",
|
|
120
|
+
name: "rekey",
|
|
121
|
+
zh: "重生开发态本机密钥并重封 dev jwt_secret(不动 tracked config)",
|
|
122
|
+
en: "Regenerate the dev-local keypair and re-seal the dev jwt_secret (tracked config untouched)",
|
|
123
|
+
detail: {
|
|
124
|
+
zh: "ojm rekey\n 删除 config/secrets-*.pem → 重新 keygen → 重封 dev jwt_secret 写入 config.local.yaml\n 仅影响 gitignored 的本地文件,tracked config.yaml 零改动(方案 A' 第 8 点)\n 用于本机 dev 密钥对误删/损坏、或想轮换 dev jwt_secret;之后 ojm dev 自动用新密钥重合并重起",
|
|
125
|
+
en: "ojm rekey\n Deletes config/secrets-*.pem → keygen again → re-seal the dev jwt_secret into config.local.yaml\n Only affects gitignored local files; the tracked config.yaml is untouched (plan A' point 8)\n Use when the local dev keypair was deleted/corrupted, or to rotate the dev jwt_secret; ojm dev then re-merges and restarts with the new key",
|
|
116
126
|
},
|
|
117
127
|
},
|
|
118
128
|
{
|
package/src/vendor.ts
CHANGED
|
@@ -103,6 +103,20 @@ export function syncOjDevkit(projectRoot: string, binDir: string, log: (msg: str
|
|
|
103
103
|
return { ok: true, files };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* bin/ 下实际存在的 oj 文件名:Windows 原生二进制是 oj.exe,但也兼容无扩展名的
|
|
108
|
+
* `oj`(手动放置 / 脚本桩)。仅 win32 做回落,POSIX 恒为 `oj` 不受影响。
|
|
109
|
+
*/
|
|
110
|
+
function ojBinIn(binDir: string, platform: NodeJS.Platform = process.platform): string {
|
|
111
|
+
const primary = path.join(binDir, ojBinName(platform));
|
|
112
|
+
if (platform === "win32" && !fs.existsSync(primary)) {
|
|
113
|
+
const bare = path.join(binDir, "oj");
|
|
114
|
+
if (fs.existsSync(bare))
|
|
115
|
+
return bare;
|
|
116
|
+
}
|
|
117
|
+
return primary;
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
/**
|
|
107
121
|
* 工程内 oj 二进制路径的**唯一口径**(<projectRoot>/bin/oj)。
|
|
108
122
|
*
|
|
@@ -111,12 +125,12 @@ export function syncOjDevkit(projectRoot: string, binDir: string, log: (msg: str
|
|
|
111
125
|
* 整套同版本,路径算错会拿到半新半旧的组合。统一到这里。
|
|
112
126
|
*/
|
|
113
127
|
export function resolveOjBin(projectRoot: string): string {
|
|
114
|
-
return path.join(projectRoot, "bin"
|
|
128
|
+
return ojBinIn(path.join(projectRoot, "bin"));
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
/** 本地版本只认 .oj-version 标记;bin/oj 缺失或标记缺失都视为未安装(N6,格式 vX.Y.Z) */
|
|
118
132
|
export function readLocalVersion(binDir: string, platform: NodeJS.Platform = process.platform): string | null {
|
|
119
|
-
if (!fs.existsSync(
|
|
133
|
+
if (!fs.existsSync(ojBinIn(binDir, platform)))
|
|
120
134
|
return null;
|
|
121
135
|
const marker = path.join(binDir, ".oj-version");
|
|
122
136
|
if (!fs.existsSync(marker))
|
|
@@ -180,10 +194,23 @@ function reportProbeFailure(log: (msg: string) => void, detail: string): void {
|
|
|
180
194
|
/** npm 执行器(测试注入);返回 stdout */
|
|
181
195
|
export type NpmRun = (args: string[], cwd: string) => string;
|
|
182
196
|
|
|
183
|
-
|
|
197
|
+
/**
|
|
198
|
+
* npm 执行规格:按平台决定 execFileSync 的命令行。
|
|
199
|
+
*
|
|
200
|
+
* Windows 上 npm 实为 `npm.cmd` 批处理文件——`CreateProcess` 无法直接执行 `.cmd`
|
|
201
|
+
* (否则 `spawnSync npm.cmd EINVAL`,连 `npm view` 这步都过不去),必须由 `cmd.exe /c`
|
|
202
|
+
* 包裹运行。其余平台直接以 `npm` 执行即可。
|
|
203
|
+
*/
|
|
204
|
+
export function npmExecSpec(platform: string, args: string[]): { cmd: string, execArgs: string[] } {
|
|
205
|
+
if (platform === "win32")
|
|
206
|
+
return { cmd: "cmd.exe", execArgs: ["/c", "npm.cmd", ...args] };
|
|
207
|
+
return { cmd: "npm", execArgs: args };
|
|
208
|
+
}
|
|
184
209
|
|
|
185
|
-
const defaultRun: NpmRun = (args, cwd) =>
|
|
186
|
-
|
|
210
|
+
export const defaultRun: NpmRun = (args, cwd) => {
|
|
211
|
+
const { cmd, execArgs } = npmExecSpec(process.platform, args);
|
|
212
|
+
return execFileSync(cmd, execArgs, { cwd, encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
|
|
213
|
+
};
|
|
187
214
|
|
|
188
215
|
export interface VendorDeps {
|
|
189
216
|
run?: NpmRun
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"react-router": "7.18.2",
|
|
9
9
|
"react-router/dom": "7.18.2",
|
|
10
10
|
"@tanstack/react-query": "5.102.8",
|
|
11
|
-
"@oj-module/runtime": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract": "0.2.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
11
|
+
"@oj-module/runtime": "0.2.63",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.63",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.63",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|