@oj-module/cli 0.2.14 → 0.2.17
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/framework-dev.md +5 -0
- package/devkit/manual.md +121 -2
- package/devkit/menu-rules.md +375 -0
- package/package.json +2 -2
- package/shell-dist/assets/{index-CLdhl3Qd.js → index-B3g19Uet.js} +76 -3
- package/shell-dist/assets/runtime.js +264 -34
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/manifest-routes-derive.test.ts +102 -0
- package/src/__tests__/usage.test.ts +69 -0
- package/src/build.ts +20 -2
- package/src/bundle.ts +27 -1
- package/src/config.ts +16 -2
- package/src/index.ts +18 -1
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +2 -1
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +2 -1
- package/src/lifecycle/__tests__/manifest-contract.test.ts +70 -1
- package/src/manifest-contract.ts +25 -2
- package/src/manifest-routes-derive.ts +130 -0
- package/src/preview-config.ts +12 -5
- package/src/preview.ts +31 -10
- package/src/site-base.ts +103 -0
- package/src/usage.ts +119 -0
- package/vendor/host-versions.json +3 -3
package/src/preview-config.ts
CHANGED
|
@@ -30,6 +30,12 @@ export const PREVIEW_CONFIG_NAME = ".ojm-preview.yaml";
|
|
|
30
30
|
export interface DerivePreviewConfigOptions {
|
|
31
31
|
/** 全站产物目录(layout.distDir),写入 web 挂载的绝对路径 */
|
|
32
32
|
siteDir: string
|
|
33
|
+
/**
|
|
34
|
+
* web 挂载前缀(默认 `"/"`)。配了 `siteBase` 的工程,产物里 URL 全是
|
|
35
|
+
* `/oj/...`,preview 必须把站点挂在同一前缀上才能验证子路径部署
|
|
36
|
+
* (否则资源全 404,预览白屏)。
|
|
37
|
+
*/
|
|
38
|
+
prefix?: string
|
|
33
39
|
/**
|
|
34
40
|
* release 形态的后端模块根(api/dist)。给了且原 config 声明了 platform.modules 时,
|
|
35
41
|
* 把 `platform.root` 一并改写到该目录——特权模块的受控根必须与 `--api-path` 同源,
|
|
@@ -78,9 +84,9 @@ function isMountEntry(m: unknown): m is Record<string, unknown> {
|
|
|
78
84
|
return m !== null && typeof m === "object" && !Array.isArray(m);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
/**
|
|
82
|
-
function
|
|
83
|
-
return mounts.findIndex(m => isMountEntry(m) && m.prefix ===
|
|
87
|
+
/** 找指定 prefix 的 web 挂载条目下标;没有返回 -1 */
|
|
88
|
+
function findWebMount(mounts: unknown[], prefix: string): number {
|
|
89
|
+
return mounts.findIndex(m => isMountEntry(m) && m.prefix === prefix && "web" in m);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
/**
|
|
@@ -116,13 +122,14 @@ export function derivePreviewConfigText(configText: string, opts: DerivePreviewC
|
|
|
116
122
|
const doc = parseConfigDoc(configText, configPath);
|
|
117
123
|
|
|
118
124
|
const mounts = [...readMounts(doc, configPath)];
|
|
119
|
-
const
|
|
125
|
+
const prefix = opts.prefix ?? "/";
|
|
126
|
+
const idx = findWebMount(mounts, prefix);
|
|
120
127
|
if (idx >= 0) {
|
|
121
128
|
// 保留用户自定义键(headers 等),只接管指向与回落
|
|
122
129
|
mounts[idx] = { ...(mounts[idx] as Record<string, unknown>), web: opts.siteDir, spa: true };
|
|
123
130
|
}
|
|
124
131
|
else {
|
|
125
|
-
mounts.push({ prefix
|
|
132
|
+
mounts.push({ prefix, web: opts.siteDir, spa: true });
|
|
126
133
|
}
|
|
127
134
|
doc.mounts = mounts;
|
|
128
135
|
|
package/src/preview.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* 一律传绝对路径:oj 命令行相对 CWD、config 内相对 config 目录,混用即歧义。
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import type { ModulesConfig } from "./config";
|
|
20
21
|
import type { OjProcess } from "./oj";
|
|
21
22
|
import type { PreviewConfigFiles } from "./preview-config";
|
|
22
23
|
import { execFileSync } from "node:child_process";
|
|
@@ -28,6 +29,7 @@ import { resolveConfigPath, resolveLayout } from "./layout";
|
|
|
28
29
|
import { startOj } from "./oj";
|
|
29
30
|
import { readOjApiPrefix } from "./oj-config";
|
|
30
31
|
import { hasRootApiMount, parseMounts, writePreviewConfig } from "./preview-config";
|
|
32
|
+
import { normalizeSiteBase } from "./site-base";
|
|
31
33
|
import { resolveOjBin } from "./vendor";
|
|
32
34
|
|
|
33
35
|
export interface PreviewOptions {
|
|
@@ -40,7 +42,7 @@ export interface PreviewOptions {
|
|
|
40
42
|
/** 注入桩 oj 进程;默认 startOj */
|
|
41
43
|
ojStarter?: (configPath: string, base: string, apiPath: string, extraArgs: string[]) => OjProcess
|
|
42
44
|
/** 注入桩派生 config 落盘(测试);默认 writePreviewConfig */
|
|
43
|
-
configWriter?: (projectRoot: string, opts: { siteDir: string, port?: number, apiRoot?: string }) => PreviewConfigFiles
|
|
45
|
+
configWriter?: (projectRoot: string, opts: { siteDir: string, prefix?: string, port?: number, apiRoot?: string }) => PreviewConfigFiles
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export interface PreviewHandle {
|
|
@@ -71,7 +73,11 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
|
|
|
71
73
|
|
|
72
74
|
// DR-5:htmlTransform 存在性检测(只读 web.config.ts 的字段,不 import 变换模块——
|
|
73
75
|
// 单服务下 oj 直出 HTML,钩子不生效;执行用户代码只会白付副作用)
|
|
74
|
-
await warnHtmlTransformUnsupported(projectRoot);
|
|
76
|
+
const moduleConfig = await warnHtmlTransformUnsupported(projectRoot);
|
|
77
|
+
|
|
78
|
+
// 站点部署前缀:web.config.ts 的 siteBase("{siteBase}" → 挂载 prefix "/oj")。
|
|
79
|
+
// 产物里的 URL 已按 siteBase 重写,preview 必须挂在同一前缀上,否则白屏。
|
|
80
|
+
const sitePrefix = mountPrefixOf(moduleConfig?.siteBase);
|
|
75
81
|
|
|
76
82
|
const execOj = opts.execOj ?? ((args: string[]) => execFileSync(ojBin, args, { stdio: "inherit" }));
|
|
77
83
|
|
|
@@ -81,12 +87,13 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
|
|
|
81
87
|
|
|
82
88
|
// 派生 config 在 migrate 成功后落盘:失败即退路径零残留
|
|
83
89
|
const base = readOjApiPrefix(configPath);
|
|
84
|
-
const writer = opts.configWriter ?? ((root: string, o: { siteDir: string, port?: number, apiRoot?: string }) => writePreviewConfig(root, o));
|
|
90
|
+
const writer = opts.configWriter ?? ((root: string, o: { siteDir: string, prefix?: string, port?: number, apiRoot?: string }) => writePreviewConfig(root, o));
|
|
85
91
|
// apiRoot 交给派生器:release 下 platform.root 必须指向 api/dist(与 --api-path 同源)
|
|
86
|
-
const preview = writer(projectRoot, { siteDir, port: opts.port, apiRoot: apiDist });
|
|
92
|
+
const preview = writer(projectRoot, { siteDir, prefix: sitePrefix, port: opts.port, apiRoot: apiDist });
|
|
87
93
|
|
|
88
|
-
// / api 挂载与注入的
|
|
89
|
-
|
|
94
|
+
// / api 挂载与注入的 web 挂载配对时 oj 不开 SPA 回落(oj 语义,见 devkit 手册 §13)。
|
|
95
|
+
// 仅在站点挂根时成立:web 挂到 /oj 时两者不配对,深链回落正常。
|
|
96
|
+
if (sitePrefix === "/" && hasRootApiMount(parseMounts(readFileSync(configPath, "utf-8"), configPath)))
|
|
90
97
|
console.warn("[ojm] config 的 api 挂载位于 /:与 web 挂载配对时 oj 不做 SPA 深链回落(已知限制),深链接刷新将 404。");
|
|
91
98
|
|
|
92
99
|
const starter = opts.ojStarter ?? ((cfg: string, b: string, apiPath: string, extraArgs: string[]) =>
|
|
@@ -111,7 +118,7 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
|
|
|
111
118
|
throw error;
|
|
112
119
|
}
|
|
113
120
|
console.log(`[ojm] oj 已就绪(release/js,单服务:${base} API + 静态 SPA 同进程):http://127.0.0.1:${oj.port}`);
|
|
114
|
-
console.log(`\n[ojm] preview 已启动:http://localhost:${oj.port}`);
|
|
121
|
+
console.log(`\n[ojm] preview 已启动:http://localhost:${oj.port}${sitePrefix === "/" ? "" : sitePrefix}`);
|
|
115
122
|
console.log(`[ojm] 静态由 oj web 挂载直出(spa 深链回落);派生 config ${path.basename(preview.configPath)} 为本进程临时产物,退出自动清理`);
|
|
116
123
|
|
|
117
124
|
// 退出回收:派生 config 必须随 oj 一起消亡(test/integration 断言其不存在)
|
|
@@ -150,10 +157,11 @@ export async function runPreview(projectRoot: string, opts: PreviewOptions = {})
|
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
/**
|
|
153
|
-
* web.config.ts 配了 htmlTransform → WARN
|
|
154
|
-
*
|
|
160
|
+
* web.config.ts 配了 htmlTransform → WARN;返回已加载的配置(preview 还要读
|
|
161
|
+
* siteBase 决定挂载前缀)。加载失败(缺文件/坏配置)静默跳过:能走到 preview
|
|
162
|
+
* 的工程必然过 ojm build,config 问题会在那里先炸。
|
|
155
163
|
*/
|
|
156
|
-
async function warnHtmlTransformUnsupported(projectRoot: string): Promise<
|
|
164
|
+
async function warnHtmlTransformUnsupported(projectRoot: string): Promise<ModulesConfig | undefined> {
|
|
157
165
|
try {
|
|
158
166
|
const config = await loadModulesConfig(projectRoot);
|
|
159
167
|
if (config.htmlTransform) {
|
|
@@ -163,8 +171,21 @@ async function warnHtmlTransformUnsupported(projectRoot: string): Promise<void>
|
|
|
163
171
|
+ "见 bin/devkit/api-manual.md「静态站点与 per-route meta」。",
|
|
164
172
|
);
|
|
165
173
|
}
|
|
174
|
+
return config;
|
|
166
175
|
}
|
|
167
176
|
catch {
|
|
168
177
|
// config 加载失败:不阻塞 preview(build 已先行校验过)
|
|
178
|
+
return undefined;
|
|
169
179
|
}
|
|
170
180
|
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `siteBase`(`"/oj"` / `"/oj/"` / 空)→ oj 挂载 prefix(`"/oj"` / `"/"`)。
|
|
184
|
+
*
|
|
185
|
+
* 两者只差尾斜杠:`siteBase` 是 URL 拼接口径(恒带尾斜杠),oj 的 mount
|
|
186
|
+
* prefix 是路径口径(不带尾斜杠,见模板 config 的 `/api`)。
|
|
187
|
+
*/
|
|
188
|
+
function mountPrefixOf(siteBase: string | undefined): string {
|
|
189
|
+
const normalized = normalizeSiteBase(siteBase);
|
|
190
|
+
return normalized === "/" ? "/" : normalized.slice(0, -1);
|
|
191
|
+
}
|
package/src/site-base.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 站点部署 base(子路径部署,2026-10-21)。
|
|
3
|
+
*
|
|
4
|
+
* 问题:`ojm build` 产出的全站(shell-dist 拷来的 index.html + assets/)里,
|
|
5
|
+
* 所有 URL 都是**站点根绝对路径**(`/assets/react.js`、`/favicon.ico`)。站点
|
|
6
|
+
* 只要不挂在域名根(oj `mounts: [{ prefix: /oj, web: dist, spa: true }]`),
|
|
7
|
+
* 浏览器就会去宿主机根取资源 → 全部 404 → 首页白屏;同时 react-router 的
|
|
8
|
+
* basename 也必须是 `/oj`,否则深链刷新匹配不到任何路由。
|
|
9
|
+
*
|
|
10
|
+
* 解法(两件事,都在构建期一次性完成):
|
|
11
|
+
* 1. 重写 index.html 里的站内绝对 URL,统一加上 `web.config.ts` 的 `siteBase`;
|
|
12
|
+
* 2. 注入 `window.__OJ_BASE__`,供 runtime 与 shell 在运行期取真值
|
|
13
|
+
* (`@oj-module/runtime` 的 `resolveSiteBase()`,见其 utils/site-base.ts)。
|
|
14
|
+
*
|
|
15
|
+
* 为什么不用 `<base href>`:shell 的 CSP 含 `base-uri 'none'`(P6.2),
|
|
16
|
+
* 而且 `<base>` 会连带改变所有相对 URL 的解析,语义过宽。
|
|
17
|
+
*
|
|
18
|
+
* 为什么不用运行期改写:oj 只做静态直出(web 挂载),没有 HTML 模板层;
|
|
19
|
+
* 构建期是唯一能同时改 HTML 与 URL 的时点。
|
|
20
|
+
*
|
|
21
|
+
* 未配置 `siteBase`(默认)时本模块**逐字节返回原 HTML**——根路径部署的产物
|
|
22
|
+
* 与 0.2.16 完全一致,零回归风险。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const ROOT_BASE = "/";
|
|
26
|
+
|
|
27
|
+
/** 归一化为「以 `/` 开头且以 `/` 结尾」;空值与 `"/"` 归为根路径 */
|
|
28
|
+
export function normalizeSiteBase(raw: string | null | undefined): string {
|
|
29
|
+
if (!raw)
|
|
30
|
+
return ROOT_BASE;
|
|
31
|
+
const trimmed = raw.trim();
|
|
32
|
+
if (!trimmed || trimmed === ROOT_BASE)
|
|
33
|
+
return ROOT_BASE;
|
|
34
|
+
const withLeading = trimmed.startsWith(ROOT_BASE) ? trimmed : `${ROOT_BASE}${trimmed}`;
|
|
35
|
+
return withLeading.endsWith(ROOT_BASE) ? withLeading : `${withLeading}${ROOT_BASE}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 站内绝对路径加 base(已加过的幂等;`//host`、`http(s)://` 等原样返回) */
|
|
39
|
+
export function withSiteBase(url: string, base: string): string {
|
|
40
|
+
if (!url.startsWith(ROOT_BASE) || url.startsWith("//"))
|
|
41
|
+
return url;
|
|
42
|
+
const normalized = normalizeSiteBase(base);
|
|
43
|
+
if (normalized === ROOT_BASE || url.startsWith(normalized))
|
|
44
|
+
return url;
|
|
45
|
+
return `${normalized.slice(0, -1)}${url}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** importmap 里所有映射值加 base(保留 JSON 值类型与顺序) */
|
|
49
|
+
function rewriteImportmap(html: string, base: string): string {
|
|
50
|
+
return html.replace(
|
|
51
|
+
/(<script type="importmap"[^>]*>)([\s\S]*?)(<\/script>)/,
|
|
52
|
+
(full, open: string, body: string, close: string) => {
|
|
53
|
+
let parsed: { imports?: Record<string, string> };
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(body);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// 不是合法 JSON:宁可让构建失败,也不静默产出会白屏的 HTML
|
|
59
|
+
throw new Error("[ojm] index.html 的 importmap 不是合法 JSON,无法按 siteBase 重写");
|
|
60
|
+
}
|
|
61
|
+
if (!parsed.imports)
|
|
62
|
+
return full;
|
|
63
|
+
const imports: Record<string, string> = {};
|
|
64
|
+
for (const [specifier, url] of Object.entries(parsed.imports))
|
|
65
|
+
imports[specifier] = withSiteBase(url, base);
|
|
66
|
+
return `${open}${JSON.stringify({ ...parsed, imports }, null, 2)}${close}`;
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 重写全站 index.html:
|
|
73
|
+
* - importmap 映射值、`<script src>`、`<link href>` 统一加 `siteBase`;
|
|
74
|
+
* - `<head>` 内注入 `window.__OJ_BASE__`(**复用 importmap 的 nonce**,
|
|
75
|
+
* CSP 是 `script-src 'self' … 'nonce-xxx'`,无 nonce 的内联脚本会被直接拦掉)。
|
|
76
|
+
*/
|
|
77
|
+
export function rewriteIndexHtml(html: string, siteBase: string | undefined): string {
|
|
78
|
+
const base = normalizeSiteBase(siteBase);
|
|
79
|
+
// 根路径部署:原样返回(保持与既有产物逐字节一致)
|
|
80
|
+
if (base === ROOT_BASE)
|
|
81
|
+
return html;
|
|
82
|
+
|
|
83
|
+
const nonce = html.match(/<script type="importmap"[^>]*\snonce="([^"]*)"/)?.[1];
|
|
84
|
+
if (!nonce) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"[ojm] 站点 index.html 缺少带 nonce 的 importmap,无法注入 window.__OJ_BASE__"
|
|
87
|
+
+ "(CSP 会拦掉无 nonce 的内联脚本,子路径部署下路由 basename 会退化成 \"/\")。"
|
|
88
|
+
+ "请确认 shell-dist 是最新构建产物(pnpm build:shell)。",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let out = rewriteImportmap(html, base);
|
|
93
|
+
// 属性值:只要以单个 "/" 开头(排除 "//host")。importmap 是脚本正文、非属性,
|
|
94
|
+
// 上一步已处理,二者不重叠。
|
|
95
|
+
out = out.replace(/(\b(?:href|src)=")(\/[^/"][^"]*|\/")/g, (_m, attr: string, url: string) =>
|
|
96
|
+
`${attr}${withSiteBase(url, base)}`);
|
|
97
|
+
|
|
98
|
+
const injected = `<script nonce="${nonce}">window.__OJ_BASE__=${JSON.stringify(base)};</script>`;
|
|
99
|
+
// 注入点选在 importmap 之前:importmap 本身必须在任何 module script 之前,
|
|
100
|
+
// 而内联 classic script 是解析期同步执行,因此宿主 main 与模块 entry 读
|
|
101
|
+
// `window.__OJ_BASE__` 时它一定已经在位。
|
|
102
|
+
return out.replace("<script type=\"importmap\"", `${injected}\n\t\t<script type="importmap"`);
|
|
103
|
+
}
|
package/src/usage.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ojm` 用法文本(抽为纯函数供测试断言;index.ts 仅负责打印与退出码)。
|
|
3
3
|
*/
|
|
4
|
+
import process from "node:process";
|
|
4
5
|
|
|
5
6
|
export function usageText(): string {
|
|
6
7
|
return `@oj-module/cli
|
|
@@ -89,3 +90,121 @@ export function usageText(): string {
|
|
|
89
90
|
例:ojm test --anonymous / ojm migrate --db report / ojm openapi --check
|
|
90
91
|
`;
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 单命令帮助文本(供 `ojm <cmd> --help` / `ojm help <cmd>` 打印)。
|
|
96
|
+
* key 可为「命令」或「命令 子命令」(market 的子命令)。查不到具体子命令时回退到命令级。
|
|
97
|
+
* 内容与 usageText 同源,但按命令拆分以便独立打印。
|
|
98
|
+
*/
|
|
99
|
+
const COMMAND_HELP: Record<string, string> = {
|
|
100
|
+
"init": `ojm init [dir] [--yes]
|
|
101
|
+
前后端一体化工程脚手架(幂等补缺:重跑只补缺失文件,永不覆盖已有文件)`,
|
|
102
|
+
"dev": `ojm dev [port]
|
|
103
|
+
启动开发服务器(/api 反代 oj + 模块重建 + SSE 刷新)
|
|
104
|
+
开发模式快捷键(仅 TTY 终端,类 Vite):
|
|
105
|
+
r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开
|
|
106
|
+
q 退出(后端 oj 一并终止)
|
|
107
|
+
port 缺省 5174(oj 后端端口以 api/config.yaml 的 server.port 为准)`,
|
|
108
|
+
"build": `ojm build [--bundle] [--out <dir>]
|
|
109
|
+
构建后端(oj build)与模块产物(含全站合并)
|
|
110
|
+
--bundle 方案包构建:manifest.yaml 契约校验(fail-fast)→ 确定性 tgz
|
|
111
|
+
→ dist/<name>-<version>.tgz + sha256(市场安装器的上游产物)
|
|
112
|
+
--out <dir> --bundle 输出目录(缺省 dist/)`,
|
|
113
|
+
"preview": `ojm preview [port]
|
|
114
|
+
生产形态预览(migrate → oj serve 单服务:API 与静态 SPA 同进程,深链回落)
|
|
115
|
+
静态由 oj web 挂载直出;ojm 不起第二个服务、不做反代
|
|
116
|
+
port 缺省取 config 的 server.port`,
|
|
117
|
+
"api": `ojm api [dir] [--check] [--docs] [--exempt <path>]
|
|
118
|
+
契约代码生成(client / routes / openapi / stub)
|
|
119
|
+
--check 四重对账(生成物同步 / route 双向 / routes.js / WS 路由鉴权)
|
|
120
|
+
--exempt <path> 指定豁免清单,覆盖默认 api/.ojm-api-exempt.json
|
|
121
|
+
--docs 出自包含文档站(单文件离线可看)
|
|
122
|
+
dir 缺省当前目录;显式目录须先存在`,
|
|
123
|
+
"info": `ojm info
|
|
124
|
+
输出版本矩阵与模块清单(报障用)`,
|
|
125
|
+
"vendor": `ojm vendor [tag] [--force]
|
|
126
|
+
下载/重装 oj vendor(经 npm 包 @oj-bin/oj;tag 缺省取最新,形如 v0.1.13)
|
|
127
|
+
--force 即使已存在也重新下载`,
|
|
128
|
+
"merge": `ojm merge <out.json> <in1.json> [in2.json ...]
|
|
129
|
+
合并多团队清单(清单先后 = 接管优先级)`,
|
|
130
|
+
"install": `ojm install <file.tgz | name[@version]> [--url <u>] [--user <u>] [--pass <p>] [--name <n>] [--version <v>] [--locale <zh-CN|en-US>]
|
|
131
|
+
安装方案包到目标站点:本地 tgz 走 file 上传(<name>-<version>.tgz 文件名推导身份,可 --name/--version 覆盖);name[@version] 走 CDN 市场源
|
|
132
|
+
登录 → 提交 → 轮询任务至终态并打印结果(失败附原因 + 指引,--locale 切双语)
|
|
133
|
+
站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS`,
|
|
134
|
+
"enable": `ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]
|
|
135
|
+
启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)
|
|
136
|
+
站点寻址同上`,
|
|
137
|
+
"disable": `ojm enable|disable <name> [--url <u>] [--user <u>] [--pass <p>]
|
|
138
|
+
启用/禁用已装方案(四处同写:锁 / sidecar / 路由清单 / 模块清单)
|
|
139
|
+
站点寻址同上`,
|
|
140
|
+
"uninstall": `ojm uninstall <name> [--url <u>] [--user <u>] [--pass <p>]
|
|
141
|
+
卸载已装方案(四处同删 + 权限级联清理 + 版本目录移除;站点至少保留一个已锁模块)
|
|
142
|
+
站点寻址同上`,
|
|
143
|
+
"trust": `ojm trust ls|add|revoke|confirm [--url <u>] [--user <u>] [--pass <p>]
|
|
144
|
+
发布方信任库管理(站点 .solutions/trusted-keys.yaml):
|
|
145
|
+
ls <key_id 前缀?> 列出已信任 key(含 sha256 指纹分组展示)
|
|
146
|
+
add <key_id> <pub> 新增信任(64 hex 公钥;首次绑定不可改)
|
|
147
|
+
revoke <key_id> 吊销(阻断新装/升级,不动已装方案)
|
|
148
|
+
confirm --task <id> TOFU 确认:把 needs_trust 终态待确认 key 入库
|
|
149
|
+
站点寻址:--url/--user/--pass,或环境变量 OJM_URL / OJM_USER / OJM_PASS`,
|
|
150
|
+
"trust ls": `ojm trust ls [key_id 前缀?] [--url <u>] [--user <u>] [--pass <p>]
|
|
151
|
+
列出已信任 key(含 sha256 指纹分组展示)`,
|
|
152
|
+
"trust add": `ojm trust add <key_id> <pub> [--url <u>] [--user <u>] [--pass <p>]
|
|
153
|
+
新增信任(64 hex 公钥;首次绑定不可改)`,
|
|
154
|
+
"trust revoke": `ojm trust revoke <key_id> [--url <u>] [--user <u>] [--pass <p>]
|
|
155
|
+
吊销(阻断新装/升级,不动已装方案)`,
|
|
156
|
+
"trust confirm": `ojm trust confirm --task <id> [--url <u>] [--user <u>] [--pass <p>]
|
|
157
|
+
TOFU 确认:把 needs_trust 终态待确认 key 入库`,
|
|
158
|
+
"keygen": `ojm keygen --id <publisher.id> [--out <dir>] [--yes]
|
|
159
|
+
生成 Ed25519 签名密钥对:私钥默认 ~/.ojm/keys/<id>/ 永不出目录(--out 指到工程树内须 --yes 显式确认——私钥入仓即泄露);打印公钥 hex 供 ojm trust add / 模板回填`,
|
|
160
|
+
"sign": `ojm sign <file.tgz> [--id <publisher.id>] [--key <pem>] [--check]
|
|
161
|
+
detached 侧车签名:产出 <tgz>.sig(ojm install 自动探测随包上传)
|
|
162
|
+
私钥缺省取 ~/.ojm/keys/<id>/private.pem(--key 指定 pem 时须同时 --id)
|
|
163
|
+
--check 离线自检签名(不必靠宿主装失败发现签错)`,
|
|
164
|
+
"publish": `ojm publish <dir> [--id <key>] [--key <pem>] [--check]
|
|
165
|
+
索引发布:重算 <dir>/bundles.yaml + manifests.yaml(确定性 stringify,消弭格式漂移)→ 对两份索引各产出 detached 侧车签名 <kind>.yaml.sig(与 archive .sig 同构,公钥取 ~/.ojm/keys/<id>/private.pem,缺省 --id 推导)→ 写回 <dir>
|
|
166
|
+
--check 离线自检两份索引签名`,
|
|
167
|
+
"market": `ojm market <subcommand> [opts]
|
|
168
|
+
本地模拟 CDN 与市场链路子命令:setup / publish / build-index / verify / trial
|
|
169
|
+
详见各子命令帮助:ojm market setup --help 等`,
|
|
170
|
+
"market setup": `ojm market setup [--url <站点地址>] [--user <u> --pass <p>] [--new-pass <p>]
|
|
171
|
+
把系统设置 framework.market_cdn_url 指向本地模拟 CDN(工程根 cdn/,由 api/config.yaml 的 /cdn 挂载直出)。首跑若账号处于首次改密期(seed admin must_change=1),会自动改密换全权 token 后再写入`,
|
|
172
|
+
"market publish": `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]
|
|
173
|
+
把方案包落进 cdn/:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级 detached 签名 <name>-<version>.tgz.sig
|
|
174
|
+
不碰索引(索引由 build-index 扫描重建,多人发布才不会互相覆盖)`,
|
|
175
|
+
"market build-index": `ojm market build-index [--id <key>] [--key <pem>] [--check]
|
|
176
|
+
扫描 cdn/*.tgz、提取包内 manifest,全量重建 bundles.yaml + manifests.yaml 并签名
|
|
177
|
+
结果只取决于目录内容:连续执行字节一致`,
|
|
178
|
+
"market verify": `ojm market verify [--file <tgz>] [--rebuild-index]
|
|
179
|
+
离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出
|
|
180
|
+
--rebuild-index 额外比对「扫描重建的索引」与盘上索引,检出落了包却没重建的漂移
|
|
181
|
+
注意:结论恒带边界声明——校验通过只说明来源与完整性,不构成内容安全性背书`,
|
|
182
|
+
"market trial": `ojm market trial [--fixture <name>@<version>] [--keep]
|
|
183
|
+
在一次性隔离站点(随机端口 + 一次性 DB/密钥/凭据/信任库/CDN)里跑完整安装,跑完即毁;--keep 保留现场供排查。用于确认「这个包到底能不能装」,不污染你的工程`,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** 取单命令帮助;返回 undefined 表示未知命令。 */
|
|
187
|
+
export function getCommandHelp(command: string, subcommand?: string): string | undefined {
|
|
188
|
+
if (subcommand) {
|
|
189
|
+
const specific = COMMAND_HELP[`${command} ${subcommand}`];
|
|
190
|
+
if (specific)
|
|
191
|
+
return specific;
|
|
192
|
+
}
|
|
193
|
+
return COMMAND_HELP[command];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 解析「帮助请求」:`ojm <cmd> --help` / `ojm <cmd> -h` / `ojm help <cmd>` 三种形态。
|
|
198
|
+
* 返回 null 表示本次调用不是帮助请求;否则带出要查的命令与子命令(可能没有)。
|
|
199
|
+
* 抽为纯逻辑(不碰 process.exit、不读模块外状态)便于单测。
|
|
200
|
+
*/
|
|
201
|
+
export function resolveHelpRequest(argv: string[] = process.argv.slice(2)): { cmd?: string, sub?: string } | null {
|
|
202
|
+
const helpRequested = argv.some(a => a === "--help" || a === "-h") || argv[0] === "help";
|
|
203
|
+
if (!helpRequested)
|
|
204
|
+
return null;
|
|
205
|
+
const positional = argv.filter(a => a !== "--help" && a !== "-h" && !a.startsWith("-"));
|
|
206
|
+
if (positional[0] === "help")
|
|
207
|
+
positional.shift();
|
|
208
|
+
const [cmd, sub] = positional;
|
|
209
|
+
return { cmd, sub };
|
|
210
|
+
}
|
|
@@ -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.17",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.17",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.17",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|