@oj-module/cli 0.2.56 → 0.2.62
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/README.md +7 -4
- package/devkit/SKILL.md +4 -1
- package/devkit/manual.md +4 -2
- package/devkit/module-dev-and-cdn-playground.md +7 -0
- package/devkit/publish-walkthrough.md +199 -0
- package/package.json +2 -2
- package/shell-dist/assets/{index-CImwskAz.js → index-BKtXxIXz.js} +32 -2
- package/shell-dist/assets/runtime.js +6 -6
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/port-check.test.ts +40 -0
- package/src/__tests__/vendor-devkit.test.ts +87 -0
- package/src/__tests__/vendor.test.ts +39 -0
- package/src/build/backend.ts +131 -0
- package/src/build/module-build.ts +364 -0
- package/src/build/module-def.ts +400 -0
- package/src/build/site-merge.ts +33 -0
- package/src/build.ts +16 -885
- package/src/bundle.ts +61 -0
- package/src/cli.ts +11 -1
- 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 +27 -39
- package/src/lifecycle/__tests__/bundle-summary.test.ts +106 -0
- package/src/lifecycle/__tests__/bundle.test.ts +24 -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 +66 -5
- package/templates/openapi.json +7 -0
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +8 -3
- package/templates/src/app/api/_platform/_lifecycle/steps/register.ts +53 -0
- package/templates/src/app/api/_platform/_shared/service/domain/roles.ts +4 -1
- package/templates/src/app/api/_platform/_shared/service/domain/solution-perms.ts +7 -3
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +3 -0
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +3 -0
- package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +8 -0
- package/templates/src/app/api/_platform/schema.yaml +2 -0
- package/templates/src/app/api/system/roles/api.ts +2 -1
- package/templates/src/app/web/system/__client/api.schemas.ts +1 -0
- package/templates/src/app/web/system/pages/roles/index.tsx +7 -1
- package/templates/tests/lifecycle/role-title-translate.spec.ts +171 -0
- package/templates/tests/rbac-roles.spec.ts +9 -0
- package/vendor/host-versions.json +3 -3
package/devkit/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| 侧 | 本仓位置 | 用户工程里的落点 | 分发链 |
|
|
11
11
|
|---|---|---|---|
|
|
12
12
|
| ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 9 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包 → `ojm init` 第 4b 步 |
|
|
13
|
-
| oj API 开发(后端 handler、全局对象、配置、测试) | `oj-api-dev/` 子目录(快照) | `.claude/skills/oj-api-dev/` | **oj 二进制自带**(`bin/devkit/`)→ `ojm init` 第 4 步 |
|
|
13
|
+
| oj API 开发(后端 handler、全局对象、配置、测试) | `oj-api-dev/` 子目录(快照) | `.claude/skills/oj-api-dev/` | **oj 二进制自带**(`bin/devkit/`)→ `ojm init` 第 4 步 / `ojm vendor` 装·升后同步(0.2.57 起共用 `syncOjDevkit`,全量 `.md` **覆盖**——文档始终与已装 oj 版本一致) |
|
|
14
14
|
|
|
15
15
|
> **`oj-api-dev/` 子目录是快照,不是本仓维护的文档**:它与本机 oj 的
|
|
16
16
|
> `~/.local/share/oj/devkit/`(oj release 下载)逐字节一致,改它没有意义——
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|---|---|
|
|
26
26
|
| `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
|
|
27
27
|
| `manual.md` | 模块开发手册(14 章):工程结构(module-root 单树 + 两处产物根;含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读;§1.8 `#` 别名与 TS 投影)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发(含 §11.1.1 引用式方案)、签名与信任、§13.1.1 web.route_prefix 自定义根路径、**§14 组织架构与通讯录**(内置部门/成员底座 + 共享读面 7 端点 + `scope` 两级门禁 + 归属权四条规则 + 源适配器接点,选人与按部门算人都读它) |
|
|
28
|
+
| `publish-walkthrough.md` | 方案包发布全流程(**新手照做版**):构建打包 → keygen 签名 → `market publish` 落 CDN → build-index → verify/trial 验证 → install + TOFU,含多人各自独立发包纪律与失败速查(手册 §11/§12 的过程版) |
|
|
28
29
|
| `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
|
|
29
30
|
| `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路五步跑通(setup → publish → build-index → verify → 安装) |
|
|
30
31
|
| `sql-builder-missing-capabilities-pr.md` | 构造器能力缺口 PR 参考(**仅框架仓库**):7 项诉求的闭环记录 |
|
|
@@ -52,6 +53,8 @@
|
|
|
52
53
|
| `api-manual.md` | 后端开发手册(13 章):模块约定、全局对象 API、鉴权租户、测试、配置、构建发布 |
|
|
53
54
|
| `scenarios.md` | 场景速查(照抄就能跑) |
|
|
54
55
|
| `sql-builder-guide.md` | SQL 构造器专题指引(v0.1.82) |
|
|
56
|
+
| `db-guide.md` | db 新人手册:配库 → 建表 → 增删改查 → 事务的上手路径与安全边界(逐 API 穷举仍以 api-manual 为准) |
|
|
57
|
+
| `deploy-tunnel-recipe.md` | 部署级隧道 recipe:跳板机后的库在部署层打通的配方(平台不做进程内 SSH 隧道) |
|
|
55
58
|
| `oj-test-guide.md` | `oj test` L1 测试专题指引 |
|
|
56
59
|
| `global.d.ts` / `oj-modules.d.ts` | handler 全局对象 TS 类型 + `#` 导入别名 ambient 兜底 |
|
|
57
60
|
| `oidc-implementation.md` / `oidc-integration.md` | OIDC 演示走读与集成说明 |
|
|
@@ -92,8 +95,8 @@ cp ~/.local/share/oj/devkit/SKILL.md ~/.local/share/oj/devkit/api-manual.md .cla
|
|
|
92
95
|
`api-manual.md` + 有则拷的 `sql-builder-guide.md` / `oj-test-guide.md` +
|
|
93
96
|
`global.d.ts` → `.claude/skills/oj-api-dev/`;
|
|
94
97
|
- **第 4b 步**:从 cli 包内置 `devkit/`(prepack 由本目录顶层同步)拷
|
|
95
|
-
`SKILL.md` / `manual.md` / `README.md` / `wecom-daikaifa.md` / `wecom-internal-app.md`
|
|
96
|
-
→ `.claude/skills/ojm-module-dev/`。
|
|
98
|
+
`SKILL.md` / `manual.md` / `README.md` / `wecom-daikaifa.md` / `wecom-internal-app.md` /
|
|
99
|
+
`publish-walkthrough.md` → `.claude/skills/ojm-module-dev/`。
|
|
97
100
|
|
|
98
101
|
```mermaid
|
|
99
102
|
flowchart LR
|
|
@@ -110,7 +113,7 @@ flowchart LR
|
|
|
110
113
|
`.claude/skills/ojm-module-dev/` 再 init)。源文件只在仓库 `docs/devkit/` 顶层一处维护。
|
|
111
114
|
- **oj 侧**:手册与 skill 随 oj 版本发布;升级 oj 后用新 `bin/devkit/` 覆盖旧拷贝。
|
|
112
115
|
框架贡献者要把本仓快照同步到新版:从 `~/.local/share/oj/devkit/` 整目录覆盖
|
|
113
|
-
`docs/devkit/oj-api-dev/`(
|
|
116
|
+
`docs/devkit/oj-api-dev/`(oj v0.1.116 起 12 个文件,保持逐字节一致以便对账)。
|
|
114
117
|
|
|
115
118
|
> 本目录顶层 9 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
|
|
116
119
|
> 其它文档。`oj-api-dev/` 子目录是 oj 侧快照,同样自包含(其自身 README 即索引),
|
package/devkit/SKILL.md
CHANGED
|
@@ -18,7 +18,8 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
|
|
|
18
18
|
1. **读章**:建模块 → 手册 §2;加接口(handler .schema → oj openapi → 生成 → 对接)→ §3;
|
|
19
19
|
接管通知/上传/菜单 → §4;换布局或加插槽 → §5;排查 → §8;
|
|
20
20
|
**组织架构 / 通讯录(选人、按部门算收件人、接 LDAP 等新来源)→ §14**;
|
|
21
|
-
**接通企业微信(代开发 / 企业内部应用 / 两套共存)→ `wecom-daikaifa.md` + `wecom-internal-app.md
|
|
21
|
+
**接通企业微信(代开发 / 企业内部应用 / 两套共存)→ `wecom-daikaifa.md` + `wecom-internal-app.md`**;
|
|
22
|
+
**把方案包发出去(打包/签名/CDN/安装一条链,新手照敲)→ `publish-walkthrough.md`**。
|
|
22
23
|
2. **建目录**:一个目录 = 一个模块——前端一侧 `src/<模块名>/web/`(`entry.ts` 必备,
|
|
23
24
|
`pages/`、`locales/` 按需),后端一侧 `src/<模块名>/api/`(目录镜像路由的 `api.ts`,
|
|
24
25
|
动词挂 `.schema`)。两侧可只存在一侧。
|
|
@@ -143,6 +144,8 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
|
|
|
143
144
|
- [ ] `web.config.ts` 的 `modules` 里已登记(`enabled` 缺省即启用;module-root 下扫描已能捡到,登记用于排序/覆盖)
|
|
144
145
|
- [ ] 路由 `handle.layout` 选对了(有页面用 `"container"`;无页面模块 `routes: []`)
|
|
145
146
|
- [ ] 菜单标题用命名空间语法 `"<模块名>:menu.<key>"`,且 `locales/<lang>.json` 里有这个 key
|
|
147
|
+
- [ ] manifest 声明了 `roles[]` → 每个 `title_key` 用 `"<模块名>:role.<slug>"` 形状(ns 必须 = 模块名),
|
|
148
|
+
且双语 locale 里都有对应嵌套 key `{"role":{…}}`——安装时解析成 `sys_role.title`,缺失则存裸 key(不失败)
|
|
146
149
|
- [ ] 有接口 → handler 动词挂了 `.schema`(入参)+ 平级 `.response`(出参,oj v0.1.72+)→ `oj openapi` + `ojm api` 跑过 → onInit 里 `create<Module>Client(ctx)`
|
|
147
150
|
- [ ] 接管了某功能 → provider 方法数与类型定义**一个不差**
|
|
148
151
|
- [ ] 有后端 → handler 四红线过一遍(manual §1.7:respond 收口 / import 深度 / 无模块级状态 / 无模块级运行时全局)
|
package/devkit/manual.md
CHANGED
|
@@ -1446,6 +1446,7 @@ pnpm dev # 开发:devServer + oj 后端(src/<模块>/api 保
|
|
|
1446
1446
|
# Ctrl+C(SIGINT)/ kill(SIGTERM)终止 dev 时,前端与后端 oj 一起终止:
|
|
1447
1447
|
# 先关 SSE 通道 → 停 oj 子进程 → 打印「后端 oj 已随前端一起终止」,不留 oj 孤儿占端口
|
|
1448
1448
|
pnpm build # = ojm build:oj build(生成 routes.js)+ 前端全站合并到 web/dist
|
|
1449
|
+
# 末尾打印用户方案摘要:哪些方案已构建、可 --bundle 打包;没进构建的方案点名(查登记/enabled)
|
|
1449
1450
|
pnpm preview # = ojm preview:oj migrate(verify 闸)→ oj serve 单服务(API + 静态 SPA 同进程)
|
|
1450
1451
|
pnpm typecheck # = tsc --noEmit(前端工程)
|
|
1451
1452
|
pnpm typecheck:api # = tsc --noEmit -p src/tsconfig.json(后端工程,见 §6.5)
|
|
@@ -1833,8 +1834,9 @@ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登
|
|
|
1833
1834
|
一个脚手架可放多份用户方案(`ojm new` 逐个生成,各自一份 `src/<name>/solution.yaml`),
|
|
1834
1835
|
`ojm build --bundle` 无参时全部打包、逐个校验独立出包;框架自带方案自动排除(名单见
|
|
1835
1836
|
`src/.ojm-builtin.json`,`ojm info` 默认折叠自带模块)。
|
|
1836
|
-
|
|
1837
|
-
[`
|
|
1837
|
+
**从开发到发布的完整过程视角(新手照做版,含签名/私钥纪律与多人各自发包纪律)见本目录**
|
|
1838
|
+
[`publish-walkthrough.md`](./publish-walkthrough.md);仓内深度过程视角另见
|
|
1839
|
+
[`docs/guides/solution-dev-build-publish.md`](../guides/solution-dev-build-publish.md)(不随 cli 包发货)。
|
|
1838
1840
|
|
|
1839
1841
|
### 11.1 打包:`ojm build --bundle`
|
|
1840
1842
|
|
|
@@ -50,6 +50,13 @@
|
|
|
50
50
|
|
|
51
51
|
所以「菜单出现两条 A」这种事不会发生——菜单条目按模块名唯一。
|
|
52
52
|
|
|
53
|
+
> **角色名自带翻译(0.2.57 起)**:`roles[].title_key` 落库前按站点语言解析成可读文本存
|
|
54
|
+
> `sys_role.title`,原始 key 另存 `sys_role.title_key`。要求:key 形状 `<模块名>:role.<slug>`
|
|
55
|
+
> (ns 必须等于方案模块名),且两个 locale 文件里都有嵌套 key(`{"role":{"sales":"销售"}}`)。
|
|
56
|
+
> 解析数据源是包内 `web/modules/<name>/locales/`——`build --bundle` 自动从源码复制双语,
|
|
57
|
+
> 作者零动作。key 缺失/文件缺失 → title 存裸 key,**安装永不因此失败**;改 locale 文案后重新
|
|
58
|
+
> 打包升级,角色名即随升级更新。
|
|
59
|
+
|
|
53
60
|
### 3.2 但同名再装会被直接拒
|
|
54
61
|
|
|
55
62
|
安装前置校验有**方案名唯一**约束:同名方案已装 → 拒装并报占用者版本(`方案名已被已装方案「A@x.y.z」占用`)。只有升级 / 重装路径放行。这是第一道防线:从入口就不给你制造重复的机会。
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# 方案包发布全流程——新手照做版
|
|
2
|
+
|
|
3
|
+
适用 ojm ≥ 0.2.22(写作时 0.2.56)。本文自包含:从「代码写完了」到「装进站点」一条命令链照着敲,每步附期望输出与通过判据。
|
|
4
|
+
字段规范与错误码全口径见 `manual.md` §11/§12;仓内深度过程视角另见
|
|
5
|
+
`docs/guides/solution-dev-build-publish.md`(不随 cli 包发货)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 全景与前提
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart LR
|
|
13
|
+
A["① 构建 ojm build"] --> B["② 打包 ojm build --bundle"]
|
|
14
|
+
B --> C["③ 身份 ojm keygen(一次性)"]
|
|
15
|
+
C --> D["④ 签名 ojm sign --check"]
|
|
16
|
+
D --> E["⑤ 发布 ojm market publish"]
|
|
17
|
+
E --> F["⑥ 重建索引 ojm market build-index"]
|
|
18
|
+
F --> G["⑦ 验证 market verify / trial"]
|
|
19
|
+
G --> H["⑧ 安装 ojm install + TOFU"]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
信任链硬 fail-closed:未签、签错、key 不被信任的包一律拒装,没有「先装了再说」的开关。
|
|
23
|
+
**开始前确认三件事**(缺一件打包就过不去):
|
|
24
|
+
|
|
25
|
+
1. 方案契约在位:`src/<模块>/solution.yaml`(`name` / `version` / `summary`;纯前端可省 `api` 段,纯后端可省 `web` 段);
|
|
26
|
+
2. `web.config.ts` 的 `modules` 数组已登记该模块(否则构建不产出站点产物,打包报「缺少本方案的站点产物」);
|
|
27
|
+
3. 双语 locale 在位:`web/src/.../locales/zh-CN.json` 与 `en-US.json`。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. 构建与打包
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ojm build # ① 站点产物 web/dist/ + 后端产物 api/dist/
|
|
35
|
+
ojm build --bundle # ② 打包全部用户方案 → dist/<name>-<version>.tgz
|
|
36
|
+
ojm build --bundle crm # 只打 src/crm/ 这一份(多方案工程用)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
期望输出(②):
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
[ojm] 方案包已生成 → dist/<name>-<version>.tgz
|
|
43
|
+
[ojm] sha256:…(确定性打包:同源同字节)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
三个要点:
|
|
47
|
+
|
|
48
|
+
- `--bundle` **只组装、不重跑构建**——先 ① 后 ②,顺序反了装的是旧产物。
|
|
49
|
+
- **确定性打包**:同一份源码在任何机器上打出的包字节一致(sha256 相同)——这是多人协作与幂等重发的地基。
|
|
50
|
+
- 打包即校验(fail-fast):契约字段、表集对照、双语齐备、**私钥形态文件禁入包**(`*.pem`/`*.key`/`.env` 系等自动拦截)。两段式出包:校验全绿才开始写盘,失败则 dist/ 零增量。
|
|
51
|
+
|
|
52
|
+
版本号升了吗?`solution.yaml` 的 `version` 与文件名一致,**发布前必须升**(见 §7 版本冲突行)。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. 一次性:生成发布身份(keygen)
|
|
57
|
+
|
|
58
|
+
每个开发者/每个环境**各自生成一把** Ed25519 密钥对,只需做一次:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
ojm keygen --id acme.alice.macbook # 例:<团队/产品>.<人名>.<环境>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- 期望输出里有一行 **fingerprint**(`sha256(公钥)`)——**抄下来发给运维**,TOFU 核对要用(§8)。
|
|
65
|
+
- 落点 `~/.ojm/keys/<id>/`:`private.pem`(权限 0600)+ `public.hex`。
|
|
66
|
+
- key_id 必须**全小写、至少两段**(`<publisher>.<id>`),禁裸通用名如 `mykey`——违约包能签出来但装不进(`sig_key` 拒装)。
|
|
67
|
+
|
|
68
|
+
私钥纪律(逐条照做,违者 = 任何人都能以你的身份发包):
|
|
69
|
+
|
|
70
|
+
| 纪律 | 原因 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| 私钥只活在 `~/.ojm/keys/<id>/`,永不出目录 | 唯一受管位置;换机 = 重新 keygen 新 id 走信任流程 |
|
|
73
|
+
| 绝不提交进任何仓库、绝不写进方案包 | 打包自检会拦私钥文件,但别依赖最后一道闸 |
|
|
74
|
+
| 一台机器可多把 key(按项目/环境分 `<key-name>`),一次发布固定一把 | `.sig` 记 key_id,安装侧按 key_id 查信任库 |
|
|
75
|
+
| CI 签名用临时密钥对,发布后即吊销;或经 secret 注入只在内存停留 | 长期私钥不落 CI 工件/缓存 |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. 每次发布:签名
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ojm sign dist/<name>-<version>.tgz \
|
|
83
|
+
--id acme.alice.macbook \
|
|
84
|
+
--key ~/.ojm/keys/acme.alice.macbook/private.pem \
|
|
85
|
+
--registry-id oj-registry \
|
|
86
|
+
--check
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `--registry-id` **必填**:发布仓库的自拟标识(同一市场的所有发布方用同一个值,如 `oj-registry`)。它进签名规范串防「把 A 仓库的包重放到 B 仓库」;站点若配置了自身 registry 身份则必须与之一致(默认模板未配置,不比对)。
|
|
90
|
+
- 产物 = 同目录 `<tgz>.sig` 侧车文件。**签了再改包 = 签名作废**;重打包必须重签。
|
|
91
|
+
- `--check` 离线自检(本地验一遍),不必等安装失败才发现签错。期望输出:`[ojm] 自检通过…`。
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 4. 发布到 CDN
|
|
96
|
+
|
|
97
|
+
CDN 就是**一个装着固定命名文件的目录**:`<name>-<version>.tgz`(+ `.sig`)与索引。发布 = 把文件放进去 + 重建索引。推到远端真 CDN(OSS/S3)只是目录同步(rsync/工具上传),包文件名含版本、内容不可变,多人并发推不冲突。
|
|
98
|
+
|
|
99
|
+
### 4.1 单方案工程(一条命令)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
ojm market publish --real . \
|
|
103
|
+
--id acme.alice.macbook \
|
|
104
|
+
--key ~/.ojm/keys/acme.alice.macbook/private.pem \
|
|
105
|
+
--registry-id oj-registry \
|
|
106
|
+
--rebuild-index
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
期望输出:`[ojm] 包已落 CDN → cdn/<name>-<version>.tgz(N 字节)` + 发布方指纹。`cdn/` 下落齐六件:tgz / .tgz.sig / 索引两份 yaml(+ 各自 .sig)。
|
|
110
|
+
|
|
111
|
+
> 引用式方案(契约 `api` 段省略 `tables`/`deps`)会打印「引用式方案…非漏配」提示——那是合法形态,不是错误。
|
|
112
|
+
|
|
113
|
+
### 4.2 多方案工程(显式四步)
|
|
114
|
+
|
|
115
|
+
`src/` 下有多份 `solution.yaml` 时,`market publish --real .` 会被拒并提示改走显式链路(一次只发一份,防误发整批):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ojm build --bundle crm # ① 打包指定方案
|
|
119
|
+
ojm sign dist/crm-1.0.0.tgz --id acme.alice.macbook \
|
|
120
|
+
--key ~/.ojm/keys/acme.alice.macbook/private.pem \
|
|
121
|
+
--registry-id oj-registry --check # ② 签名
|
|
122
|
+
cp dist/crm-1.0.0.tgz dist/crm-1.0.0.tgz.sig cdn/ # ③ tgz + .sig 放进 cdn/
|
|
123
|
+
ojm market build-index --id <维护者key> --check # ④ 重建索引(见 §4.3)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4.3 多人协作纪律(同一仓库、不同开发者、不同环境各自发包)
|
|
127
|
+
|
|
128
|
+
`market publish` 的设计就是为多人并发而做的:**每人只写自己的 `<name>-<version>.tgz`**,没有共享可变文件,互不干扰。四条纪律:
|
|
129
|
+
|
|
130
|
+
1. **落包并发安全**:谁都可以随时 `market publish` 落自己的包,不用排队。
|
|
131
|
+
2. **索引重建要中心化**:publish 落包后**索引不自动重建**(日志会提示「多人场景请由维护者跑 `ojm market build-index`」)。包落了但没重建索引 = 站点装不到。约定一个触发点:维护者定期跑,或 CI 钩子。**不要两人同时对同一 cdn/ 跑 build-index**(重建本身设计为单执行者)。
|
|
132
|
+
3. **同版本号字节冲突会被拒**:`crm@1.0.0` 已存在且字节不一致时 publish 直接报错——内容有变就升 version 重发,同一版本号全宇宙只允许一份字节。同源重发字节一致则幂等通过。
|
|
133
|
+
4. **同名方案两把 key 是治理问题**:技术上可行(索引按名字聚合版本、信任按 key),但「这个方案名归谁」机制不裁决——不同人发**不同方案**随便;发**同一方案名**需团队事先约定(推荐:方案名归属唯一发布方,他人在该方案上协作走代码仓库而非另起 key 发布)。
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 5. 验证发布(不起站点)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
ojm market verify --rebuild-index # 五项自检:完整性/包级签/索引签×2/manifest 结构,全 ✓ 才算过
|
|
141
|
+
ojm market trial --fixture <name>@<version> # 隔离站点跑完整安装,跑完即毁,不污染工程
|
|
142
|
+
ojm market walkthrough --fixture <name>@<version> # 14 项 AC:主链 9 + 负向 5;报告落 logs/walkthrough-<ts>.md
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
walkthrough 期望 `14 PASS / 0 FAIL`。负向 5 项是**预期失败口径**:防线没拦住(恶意包装成了)才算 FAIL。
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 6. 装到站点(运维侧)
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# ① 站点以 release 形态起(dev/preview 完不成安装的热更段)
|
|
153
|
+
oj serve -c config.yaml -b /api --api-path dist/api
|
|
154
|
+
|
|
155
|
+
# ② 市场地址指向站点自供的 /cdn(默认即如此;外部真 CDN 才需要 setup 写绝对地址)
|
|
156
|
+
ojm market setup --site-url http://127.0.0.1:9778 --user superadmin --pass … --new-pass <新口令>
|
|
157
|
+
|
|
158
|
+
# ③ 安装(从 CDN 拉:name[@version];直传文件则给 tgz 路径,.sig 同目录自动随包)
|
|
159
|
+
ojm install crm@1.0.0 --site-url http://127.0.0.1:9778 --user superadmin --pass <新口令>
|
|
160
|
+
|
|
161
|
+
# ④ 首装陌生 key 必停在 needs_trust:响应 trust.fingerprint 与 §2 记下的指纹【逐字】核对
|
|
162
|
+
# 核对通道用当面/电话/既有加密聊天——不要用 CDN 页面上的指纹(那正是攻击者能改的地方)
|
|
163
|
+
ojm trust confirm --task <task-id> --site-url http://127.0.0.1:9778 --user superadmin --pass <新口令>
|
|
164
|
+
ojm install crm@1.0.0 --site-url http://127.0.0.1:9778 --user superadmin --pass <新口令> # 重跑续跑
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
通过判据:`dist/api/manifests.yaml` 出现 `<name>: <version>`;**重启一次 `oj serve`**(新方案路由要重启才注册,热更只覆盖 vars)后 `curl /api/<name>/ping` 返回 `code:0`。
|
|
168
|
+
|
|
169
|
+
第一方方案走模板内置公钥(bootstrap key),首装免 TOFU;第三方走上面的人工核对。key 轮换 = 新 key_id 重发 + 站点 `trust add` 新公钥 + `trust revoke` 旧 key(吊销只阻断新装,不动已装方案)。
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 7. 失败速查(症状 → 病根 → 药方)
|
|
174
|
+
|
|
175
|
+
| 症状(安装拒装码 / 命令报错) | 病根 | 药方 |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `sig_missing` | 没签,或 `.sig` 没随包在同目录 | `ojm sign --check` 后确认 `.sig` 与 tgz 同目录再装 |
|
|
178
|
+
| `tampered` | 签完又动过包,或传输损坏 | 重打包重签;核对 sha256 |
|
|
179
|
+
| `sig_key` | key_id 不是 `<publisher>.<id>` 全小写两段式 | 重新 keygen 合规 id,用新 key 重签 |
|
|
180
|
+
| `sig_alg` / `sig_format` | 签名算法/.sig 结构不符 | 用当前 ojm 重签(旧版本工具产物) |
|
|
181
|
+
| `needs_trust`(第五终态,非成功) | 站点第一次见这个 key | 与发布方逐字核对指纹 → `trust confirm --task <id>` → 重跑 install |
|
|
182
|
+
| publish 报「包冲突:…字节不一致」 | 同 name@version 已有字节不同的包 | **升 version 重发**;确需覆盖先删 CDN 旧 tgz |
|
|
183
|
+
| publish 报「一次只发布一份方案」 | 多方案工程走了自动链路 | §4.2 显式四步 |
|
|
184
|
+
| 打包报「缺少本方案的站点产物」 | `web.config.ts` 没登记 / 没先 `ojm build` | 登记后重跑构建再打包 |
|
|
185
|
+
| 打包报「缺少 bundle 级契约」 | 没有 `solution.yaml` | 补契约(§0 前提 1) |
|
|
186
|
+
| 装不到刚发的包 | 索引没重建 | `ojm market build-index`(§4.3 纪律 2),确认 CDN 六文件同步 |
|
|
187
|
+
| 装成后 404 | 方案路由没注册 | 重启 `oj serve`(§6 通过判据) |
|
|
188
|
+
| 私钥误入工程 | 打包自检拦下 / 被外部拷走 | 立即 `trust revoke` 该 key → 换新 key_id 重发 |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 8. 发布前 checklist
|
|
193
|
+
|
|
194
|
+
- [ ] `solution.yaml` 的 `version` 已升,与文件名一致;
|
|
195
|
+
- [ ] `ojm build` + `ojm build --bundle` 均通过,sha256 记进发布单;
|
|
196
|
+
- [ ] `ojm sign … --check` 通过,`.sig` 与 tgz 同目录;
|
|
197
|
+
- [ ] 新 key 首次发布:fingerprint 已提前交给运维(免得卡在 needs_trust);
|
|
198
|
+
- [ ] `ojm market verify` 五项全 ✓;
|
|
199
|
+
- [ ] 多人团队:索引重建触发点已约定(维护者或 CI)。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oj-module/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.62",
|
|
5
5
|
"description": "React Antd 模块工程命令行工具(含预构建宿主 shell-dist)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@faker-js/faker": "^10.4.0",
|
|
52
|
-
"@oj-module/runtime": "0.2.
|
|
52
|
+
"@oj-module/runtime": "0.2.62",
|
|
53
53
|
"@redocly/cli": "^2.51.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
"cac": "^6.7.14",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StyleProvider } from "@ant-design/cssinjs";
|
|
2
2
|
import { AntdApp, ErrorBoundary, LayoutEffects, PageError, getHostRoutes, getRoutes, loadAll, resolveApiBase, setupI18n, useAccessStore, useAuthStore, usePreferences, useUserStore } from "@oj-module/runtime";
|
|
3
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
|
+
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
4
|
import { ConfigProvider, theme } from "antd";
|
|
5
5
|
import { useEffect, useState } from "react";
|
|
6
6
|
import { createRoot } from "react-dom/client";
|
|
@@ -195,7 +195,37 @@ ${offenders.join("\n")}\n修复建议:该来源不在宿主内置白名单中
|
|
|
195
195
|
}
|
|
196
196
|
//#endregion
|
|
197
197
|
//#region shell/src/host.tsx
|
|
198
|
-
|
|
198
|
+
/**
|
|
199
|
+
* 全局失败收敛(框架层唯一构造点)。
|
|
200
|
+
*
|
|
201
|
+
* 宿主是唯一构造 QueryClient 的地方:模块页面的 useQuery/useQueryClient 上下文
|
|
202
|
+
* 都来自此处,`ojm build` 对 shell-dist 全量合并,工程侧没有注入 defaults 的缝隙。
|
|
203
|
+
* 原先裸 `new QueryClient()` 走 TanStack 默认 `retry: 3`,导致「必然失败也白等 3 次
|
|
204
|
+
* 重试 + 请求层逐次弹 toast 刷屏」。
|
|
205
|
+
*
|
|
206
|
+
* 这里两处收敛:
|
|
207
|
+
* - `retry: 1`:保留一次瞬时抖动重试,去掉默认 3 次的无效等待(mutations 默认不重试);
|
|
208
|
+
* - `QueryCache/MutationCache` onError:请求层 {@link handleErrorResponse} 已对
|
|
209
|
+
* HTTP 错误体弹 toast,此处只兜底「请求层未触达」的失败(网络中断 / 解析异常 /
|
|
210
|
+
* 非响应错误),避免与其重复弹窗。
|
|
211
|
+
*/
|
|
212
|
+
function isHttpError(error) {
|
|
213
|
+
const maybe = error;
|
|
214
|
+
return Boolean(maybe?.response && typeof maybe.response.status === "number");
|
|
215
|
+
}
|
|
216
|
+
function surfaceQueryError(error) {
|
|
217
|
+
if (isHttpError(error)) return;
|
|
218
|
+
const message = error instanceof Error && /fetch|network|timeout/i.test(error.message) ? "网络异常,请检查网络连接后重试" : error instanceof Error ? error.message : "请求失败";
|
|
219
|
+
window.$message?.error(message);
|
|
220
|
+
}
|
|
221
|
+
var queryClient = new QueryClient({
|
|
222
|
+
defaultOptions: {
|
|
223
|
+
queries: { retry: 1 },
|
|
224
|
+
mutations: { retry: 0 }
|
|
225
|
+
},
|
|
226
|
+
queryCache: new QueryCache({ onError: surfaceQueryError }),
|
|
227
|
+
mutationCache: new MutationCache({ onError: surfaceQueryError })
|
|
228
|
+
});
|
|
199
229
|
var DEMO_USER = {
|
|
200
230
|
id: "1",
|
|
201
231
|
avatar: "https://avatars.githubusercontent.com/u/47056890",
|
|
@@ -304,10 +304,10 @@ function getAppInfo() {
|
|
|
304
304
|
return {
|
|
305
305
|
"pkg": {
|
|
306
306
|
"name": "@oj-module/runtime",
|
|
307
|
-
"version": "0.2.
|
|
307
|
+
"version": "0.2.62",
|
|
308
308
|
"license": "MIT"
|
|
309
309
|
},
|
|
310
|
-
"lastBuildTime": "2026-11-
|
|
310
|
+
"lastBuildTime": "2026-11-04 08:31:17"
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
var init_get_app_info = __esmMin((() => {}));
|
|
@@ -365,9 +365,9 @@ var init_preferences$3 = __esmMin((() => {
|
|
|
365
365
|
accordion: true,
|
|
366
366
|
enableFooter: true,
|
|
367
367
|
fixedFooter: true,
|
|
368
|
-
companyName: "
|
|
369
|
-
companyWebsite: "http://github.com/
|
|
370
|
-
copyrightDate: "
|
|
368
|
+
companyName: "Omni Solution",
|
|
369
|
+
companyWebsite: "http://github.com/everpan",
|
|
370
|
+
copyrightDate: "2026",
|
|
371
371
|
ICPNumber: "",
|
|
372
372
|
ICPLink: ""
|
|
373
373
|
};
|
|
@@ -7160,7 +7160,7 @@ var init_parent_layout = __esmMin((() => {}));
|
|
|
7160
7160
|
*/
|
|
7161
7161
|
function resolveLayoutComponent(handle) {
|
|
7162
7162
|
const name = handle?.layout;
|
|
7163
|
-
if (!name) return Outlet;
|
|
7163
|
+
if (!name || name === "none") return Outlet;
|
|
7164
7164
|
const registered = getRegisteredLayout(name);
|
|
7165
7165
|
if (registered) return registered;
|
|
7166
7166
|
if (layoutRegistry[name]) return layoutRegistry[name];
|
package/shell-dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" href="/favicon.ico" />
|
|
7
7
|
<title>React Antd Admin · Shell</title>
|
|
8
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-
|
|
9
|
-
<script type="importmap" nonce="
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-SJbJrNlpPRxj8pl6EctCJA=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
|
|
9
|
+
<script type="importmap" nonce="SJbJrNlpPRxj8pl6EctCJA==">{
|
|
10
10
|
"imports": {
|
|
11
11
|
"react": "/assets/react.js",
|
|
12
12
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
.boot-skeleton:after{content:"";width:18px;height:18px;margin-left:8px;border:2px solid #d9d9d9;border-top-color:#1677ff;border-radius:50%;animation:bs-spin .8s linear infinite}
|
|
131
131
|
@keyframes bs-spin{to{transform:rotate(360deg)}}
|
|
132
132
|
</style>
|
|
133
|
-
<script nonce="
|
|
133
|
+
<script nonce="SJbJrNlpPRxj8pl6EctCJA==" data-ojm-entry="/assets/index-BKtXxIXz.js">(function(){try{var ua=(navigator.userAgent||"").toLowerCase(),p=location.pathname;while(p.length>1&&p.charAt(p.length-1)==="/")p=p.slice(0,-1);if(ua.indexOf("wxwork")>=0&&p.slice(-6)==="/login"&&!new URLSearchParams(location.search).get("wecom_ticket")){var a=(typeof window.__OJ_API_BASE__==="string"&&window.__OJ_API_BASE__.trim())||"/api";while(a.length>1&&a.charAt(a.length-1)==="/")a=a.slice(0,-1);location.replace(a+"/auth/wecom/login");return}}catch(e){}var s=document.createElement("script");s.type="module";s.src=document.currentScript.getAttribute("data-ojm-entry");document.head.appendChild(s);})();</script>
|
|
134
134
|
</head>
|
|
135
135
|
<body>
|
|
136
136
|
<div id="root"><div class="boot-skeleton">加载中…</div></div>
|
package/shell-dist/versions.json
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"react-router": "7.18.2",
|
|
8
8
|
"react-router/dom": "7.18.2",
|
|
9
9
|
"@tanstack/react-query": "5.102.8",
|
|
10
|
-
"@oj-module/runtime": "0.2.
|
|
11
|
-
"@oj-module/runtime/contract": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
10
|
+
"@oj-module/runtime": "0.2.62",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.62",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.62",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { afterAll, describe, expect, it } from "vitest";
|
|
3
|
+
import { isPortOccupied } from "../port-check";
|
|
4
|
+
|
|
5
|
+
describe("isPortOccupied", () => {
|
|
6
|
+
it("端口有监听者时返回 true", async () => {
|
|
7
|
+
const server = net.createServer();
|
|
8
|
+
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", () => resolve()));
|
|
9
|
+
const port = (server.address() as net.AddressInfo).port;
|
|
10
|
+
expect(await isPortOccupied(port)).toBe(true);
|
|
11
|
+
await new Promise<void>(resolve => server.close(() => resolve()));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("端口空闲时返回 false", async () => {
|
|
15
|
+
// 探测一批非临时端口区间的端口:localhost 上这些端口几乎不可能被占用,
|
|
16
|
+
// 命中一个即证明「空闲端口返回 false」。避开 macOS 临时端口区间(49152+,
|
|
17
|
+
// 连接会被静默丢弃而非返回 ECONNREFUSED,导致保守判为占用)。
|
|
18
|
+
let foundFree = false;
|
|
19
|
+
for (const p of [23000, 23001, 23002, 23003, 23004]) {
|
|
20
|
+
if (!(await isPortOccupied(p))) {
|
|
21
|
+
foundFree = true;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
expect(foundFree).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("默认 host 为 127.0.0.1", async () => {
|
|
29
|
+
const server = net.createServer();
|
|
30
|
+
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", () => resolve()));
|
|
31
|
+
const port = (server.address() as net.AddressInfo).port;
|
|
32
|
+
expect(await isPortOccupied(port, "127.0.0.1")).toBe(true);
|
|
33
|
+
await new Promise<void>(resolve => server.close(() => resolve()));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 防止未关闭句柄拖慢套件退出
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
// 无持久资源;占位以明确意图
|
|
40
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// BDD:devkit 全链路落地与文档同步(计划 docs/plans/20261103-1020-vendor-devkit-sync)。
|
|
2
|
+
// vendor 装/升 oj 后把 bin/devkit 文档**覆盖同步**进 .claude/skills/oj-api-dev/(R2/R3/R4);
|
|
3
|
+
// init 步骤 4 走同一出口(R5,global.d.ts 模板变体保护);ojm devkit 4b 现状回归(R1)。
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
+
import { initProject } from "../init";
|
|
9
|
+
import { syncOjDevkit } from "../vendor";
|
|
10
|
+
|
|
11
|
+
/* eslint-disable test/prefer-lowercase-title -- 计划编号是验收锚点 */
|
|
12
|
+
let root: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
root = mkdtempSync(join(tmpdir(), "ojm-devkit-sync-"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
rmSync(root, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** 模拟 @oj-bin/oj postinstall 落盘的 bin/devkit(版本标记进内容以断言覆盖方向) */
|
|
23
|
+
function makeDevkit(version: string, dir = join(root, "bin", "devkit")): void {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
writeFileSync(join(dir, "SKILL.md"), `# oj-api-dev skill ${version}\n`);
|
|
26
|
+
writeFileSync(join(dir, "api-manual.md"), `manual ${version}\n`);
|
|
27
|
+
// 旧 init 清单之外的文件——按目录扫描(R6)应收进来
|
|
28
|
+
writeFileSync(join(dir, "scenarios.md"), `scenarios ${version}\n`);
|
|
29
|
+
writeFileSync(join(dir, "global.d.ts"), `// devkit ${version}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const skillOf = (name: string) => join(root, ".claude/skills/oj-api-dev", name);
|
|
33
|
+
|
|
34
|
+
describe("syncOjDevkit(vendor/init 共用同步出口)", () => {
|
|
35
|
+
it("R2/R6 bin/devkit 齐备 → 全部 .md 同步进 skill 目录(含旧清单外文件;global.d.ts 不进)", () => {
|
|
36
|
+
makeDevkit("v2");
|
|
37
|
+
const r = syncOjDevkit(root, join(root, "bin"), () => {});
|
|
38
|
+
expect(r.ok).toBe(true);
|
|
39
|
+
expect(r.files.sort()).toEqual(["SKILL.md", "api-manual.md", "scenarios.md"]);
|
|
40
|
+
expect(readFileSync(skillOf("SKILL.md"), "utf-8")).toContain("v2");
|
|
41
|
+
expect(readFileSync(skillOf("scenarios.md"), "utf-8")).toContain("v2");
|
|
42
|
+
expect(existsSync(skillOf("global.d.ts"))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("R3 同步是覆盖语义:oj 升级后旧 skill 内容被当前版本刷新(非首拷赢)", () => {
|
|
46
|
+
makeDevkit("v1");
|
|
47
|
+
syncOjDevkit(root, join(root, "bin"), () => {});
|
|
48
|
+
writeFileSync(join(root, "bin", "devkit", "SKILL.md"), "# oj-api-dev skill v2\n");
|
|
49
|
+
syncOjDevkit(root, join(root, "bin"), () => {});
|
|
50
|
+
expect(readFileSync(skillOf("SKILL.md"), "utf-8")).toContain("v2");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("R3 bin/devkit 缺 SKILL.md → ok:false,skill 目录不创建、不抛错", () => {
|
|
54
|
+
const r = syncOjDevkit(root, join(root, "bin"), () => {});
|
|
55
|
+
expect(r.ok).toBe(false);
|
|
56
|
+
expect(existsSync(join(root, ".claude/skills/oj-api-dev"))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("ojm init 集成(注入 ojInstaller 模拟 vendor 落盘)", () => {
|
|
61
|
+
it("R1/R5 init 后两侧 skill 都在:oj devkit 同步 + ojm devkit 六文件", async () => {
|
|
62
|
+
await initProject(root, {
|
|
63
|
+
yes: true,
|
|
64
|
+
seal: async () => "ENC[test]",
|
|
65
|
+
ojInstaller: async (binDir) => {
|
|
66
|
+
makeDevkit("v1", join(binDir, "devkit"));
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
expect(readFileSync(join(root, ".claude/skills/oj-api-dev/SKILL.md"), "utf-8")).toBe("# oj-api-dev skill v1\n");
|
|
70
|
+
expect(existsSync(join(root, ".claude/skills/oj-api-dev/scenarios.md"))).toBe(true);
|
|
71
|
+
for (const name of ["SKILL.md", "manual.md", "README.md", "wecom-daikaifa.md", "wecom-internal-app.md", "publish-walkthrough.md"])
|
|
72
|
+
expect(existsSync(join(root, ".claude/skills/ojm-module-dev", name)), name).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("R5 工程 global.d.ts 保模板变体:devkit 同名文件不覆盖(init/vendor 均不碰)", async () => {
|
|
76
|
+
await initProject(root, {
|
|
77
|
+
yes: true,
|
|
78
|
+
seal: async () => "ENC[test]",
|
|
79
|
+
ojInstaller: async (binDir) => {
|
|
80
|
+
makeDevkit("v1", join(binDir, "devkit"));
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const rootGlobal = readFileSync(join(root, "global.d.ts"), "utf-8");
|
|
84
|
+
expect(rootGlobal).not.toContain("// devkit v1");
|
|
85
|
+
expect(rootGlobal).toContain(".route");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { defaultRun, npmExecSpec } from "../vendor";
|
|
4
|
+
|
|
5
|
+
vi.mock("node:child_process", () => ({
|
|
6
|
+
execFileSync: vi.fn(() => ""),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const mockedExec = vi.mocked(execFileSync);
|
|
10
|
+
|
|
11
|
+
describe("npmExecSpec / defaultRun(Windows npm.cmd 兼容)", () => {
|
|
12
|
+
it("非 Windows:直接以 npm 执行,不经 shell", () => {
|
|
13
|
+
expect(npmExecSpec("linux", ["view", "x", "version"])).toEqual({
|
|
14
|
+
cmd: "npm",
|
|
15
|
+
execArgs: ["view", "x", "version"],
|
|
16
|
+
});
|
|
17
|
+
expect(npmExecSpec("darwin", ["install", "y"])).toEqual({
|
|
18
|
+
cmd: "npm",
|
|
19
|
+
execArgs: ["install", "y"],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("windows:以 cmd.exe /c npm.cmd 包裹(修复 spawnSync npm.cmd EINVAL)", () => {
|
|
24
|
+
expect(npmExecSpec("win32", ["install", "@oj-bin/oj@0.1.86"])).toEqual({
|
|
25
|
+
cmd: "cmd.exe",
|
|
26
|
+
execArgs: ["/c", "npm.cmd", "install", "@oj-bin/oj@0.1.86"],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("defaultRun 按当前平台选择执行方式", () => {
|
|
31
|
+
defaultRun(["view", "x", "version"], "/tmp/x");
|
|
32
|
+
const platform = process.platform;
|
|
33
|
+
const expectedCmd = platform === "win32" ? "cmd.exe" : "npm";
|
|
34
|
+
const expectedArgs = platform === "win32"
|
|
35
|
+
? ["/c", "npm.cmd", "view", "x", "version"]
|
|
36
|
+
: ["view", "x", "version"];
|
|
37
|
+
expect(mockedExec).toHaveBeenCalledWith(expectedCmd, expectedArgs, expect.objectContaining({ cwd: "/tmp/x" }));
|
|
38
|
+
});
|
|
39
|
+
});
|