@oj-module/cli 0.2.30 → 0.2.43
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/manual.md +5 -5
- package/devkit/module-dev-and-cdn-playground.md +1 -1
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +2 -2
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/cli.test.ts +23 -14
- package/src/__tests__/list.test.ts +176 -0
- package/src/__tests__/registry-cli.test.ts +522 -0
- package/src/__tests__/registry-export.test.ts +319 -0
- package/src/__tests__/registry-import.test.ts +336 -0
- package/src/__tests__/registry-sign.test.ts +352 -0
- package/src/cli-output.ts +81 -0
- package/src/cli.ts +298 -39
- package/src/config-cmd.ts +100 -0
- package/src/contract/__tests__/handler-scan.test.ts +74 -2
- package/src/contract/handler-scan.ts +27 -2
- package/src/enable.ts +1 -1
- package/src/exit-codes.ts +33 -0
- package/src/index.ts +5 -9
- package/src/install.ts +120 -38
- package/src/lifecycle/__tests__/authoring.test.ts +6 -5
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +3 -2
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +5 -4
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +9 -6
- package/src/lifecycle/__tests__/helpers.ts +10 -6
- package/src/lifecycle/__tests__/install.test.ts +72 -15
- package/src/lifecycle/__tests__/manifest-contract.test.ts +40 -1
- package/src/lifecycle/__tests__/publish.test.ts +174 -33
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +9 -6
- package/src/lifecycle/__tests__/sign.test.ts +9 -8
- package/src/lifecycle/__tests__/smoke.test.ts +1 -1
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/lifecycle/sign.ts +8 -7
- package/src/list.ts +146 -0
- package/src/manifest-contract.ts +11 -5
- package/src/market/__tests__/bootstrap.test.ts +4 -1
- package/src/market/__tests__/build-index.test.ts +529 -42
- package/src/market/__tests__/fixture.test.ts +17 -1
- package/src/market/__tests__/publish.test.ts +8 -7
- package/src/market/__tests__/setup.test.ts +4 -4
- package/src/market/__tests__/sharding.test.ts +90 -0
- package/src/market/__tests__/verify.test.ts +237 -13
- package/src/market/__tests__/yank-ledger.test.ts +60 -0
- package/src/market/bootstrap.ts +8 -3
- package/src/market/build-index.ts +407 -40
- package/src/market/cdn-layout.ts +3 -2
- package/src/market/command.ts +40 -6
- package/src/market/failure-playbook.ts +1 -1
- package/src/market/fixture.ts +20 -3
- package/src/market/index-builder.ts +120 -47
- package/src/market/publish.ts +3 -1
- package/src/market/setup.ts +3 -3
- package/src/market/sharding.ts +94 -0
- package/src/market/trial.ts +1 -1
- package/src/market/verify.ts +220 -53
- package/src/market/walkthrough.ts +7 -6
- package/src/market/yank-ledger.ts +91 -0
- package/src/publish.ts +250 -38
- package/src/registry-export.ts +405 -0
- package/src/registry-import.ts +410 -0
- package/src/registry-sidecar.ts +84 -0
- package/src/registry-sign.ts +322 -0
- package/src/registry-source.ts +192 -0
- package/src/sign.ts +13 -10
- package/src/site.ts +4 -4
- package/src/trust.ts +1 -1
- package/src/uninstall.ts +14 -2
- package/src/usage.ts +130 -25
- package/src/view.ts +94 -0
- package/templates/cdn/README.md +35 -5
- package/templates/config.yaml +46 -96
- package/templates/init.include.yaml +1 -1
- package/templates/src/_platform/api/_lifecycle/atomic.ts +3 -0
- package/templates/src/_platform/api/_lifecycle/installer.ts +44 -7
- package/templates/src/_platform/api/_lifecycle/limits.ts +21 -0
- package/templates/src/_platform/api/_lifecycle/source-seq.ts +88 -0
- package/templates/src/_platform/api/_lifecycle/steps/download.ts +15 -0
- package/templates/src/_platform/api/_lifecycle/steps/extract.ts +61 -2
- package/templates/src/_platform/api/_lifecycle/steps/preflight.ts +55 -6
- package/templates/src/_platform/api/_lifecycle/steps/register.ts +2 -1
- package/templates/src/_platform/api/_lifecycle/steps/verify.ts +22 -8
- package/templates/src/_platform/api/_lifecycle/tgz-guard.ts +96 -0
- package/templates/src/_platform/api/_lifecycle/types.ts +6 -3
- package/templates/src/_platform/api/_lifecycle/verify-checks.ts +8 -0
- package/templates/src/_platform/api/_shared/service/domain/solution-perms.ts +9 -1
- package/templates/src/_platform/api/_shared/site-facade.ts +62 -11
- package/templates/src/_platform/api/manifest.yaml +1 -0
- package/templates/src/_platform/api/market/api.ts +362 -99
- package/templates/src/_platform/api/market/index-source.ts +112 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.mysql.sql +9 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.pg.sql +7 -0
- package/templates/src/_platform/api/migrations/0014__reg_source_seq.sql +8 -0
- package/templates/src/_platform/api/schema.yaml +8 -0
- package/templates/src/_platform/api/solutions/api.ts +8 -3
- package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +13 -0
- package/templates/src/oj-market/web/__tests__/InstallDialog.test.tsx +38 -0
- package/templates/src/oj-market/web/components/CardItem.tsx +10 -0
- package/templates/src/oj-market/web/components/InstallDialog.tsx +18 -0
- package/templates/src/oj-market/web/locales/en-US.json +1 -0
- package/templates/src/oj-market/web/locales/zh-CN.json +1 -0
- package/templates/src/oj-market/web/types.ts +3 -1
- package/templates/tests/lifecycle/installer.spec.ts +24 -0
- package/templates/tests/lifecycle/market-api.spec.ts +641 -85
- package/templates/tests/lifecycle/preflight.spec.ts +52 -0
- package/templates/tests/lifecycle/sig-fixtures.spec.ts +7 -4
- package/templates/tests/lifecycle/source-seq.spec.ts +55 -0
- package/templates/tests/lifecycle/steps-fs.spec.ts +166 -7
- package/templates/tests/lifecycle/tgz-guard.spec.ts +133 -1
- package/templates/tests/lifecycle/verify-checks.spec.ts +27 -13
- package/templates/tests/lifecycle/verify-report.spec.ts +3 -1
- package/templates/tests/rbac-solution-perms.spec.ts +41 -0
- package/vendor/host-versions.json +3 -3
package/devkit/manual.md
CHANGED
|
@@ -1605,7 +1605,7 @@ export default async (path: string, html: string) => {
|
|
|
1605
1605
|
|
|
1606
1606
|
| 类别 | 命令 | 谁实现 | 一句话 |
|
|
1607
1607
|
|---|---|---|---|
|
|
1608
|
-
| **ojm 原生** | `init` / `dev` / `build` / `preview` / `api` / `info` / `vendor` / `merge` / `install` / `enable` / `disable` / `uninstall` / `trust` / `keygen` / `sign` / `publish` | `ojm` 自己 | 前后端一体工程的脚手架、开发、构建、契约生成、版本/清单、oj 分发、清单合并;方案包安装面与签名信任(install/enable/disable/uninstall 见 §11,trust/keygen/sign 见 §12,publish 见 §11.2
|
|
1608
|
+
| **ojm 原生** | `init` / `dev` / `build` / `preview` / `api` / `info` / `vendor` / `merge` / `install` / `list` / `enable` / `disable` / `uninstall` / `trust` / `view` / `keygen` / `sign` / `publish` / `registry` | `ojm` 自己 | 前后端一体工程的脚手架、开发、构建、契约生成、版本/清单、oj 分发、清单合并;方案包安装面与签名信任(install/list/enable/disable/uninstall 见 §11,trust/keygen/sign 见 §12,publish 见 §11.2);`registry` 是索引/吊销清单/离线包的外置签名与流转;整条 registry 链路(发版 / 安装 / 运维 / 查询)的图文流程见 [`prd/20261026-registry-cli-commands-design.md`](../prd/20261026-registry-cli-commands-design.md) |
|
|
1609
1609
|
| **oj 透传** | `test` / `exec` / `openapi` / `migrate` / `schema` | `oj`(ojm 仅转发) | 这 5 个**就是 oj 的同名子命令**,ojm 只负责定位 `bin/oj` 并补上 `-c config.yaml` |
|
|
1610
1610
|
|
|
1611
1611
|
> **为什么只有这 5 个走透传?** oj 还有 `serve` / `build` / `secret`(`keygen`/`seal` 等)
|
|
@@ -1781,7 +1781,7 @@ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登
|
|
|
1781
1781
|
|
|
1782
1782
|
| 子命令 | 一句话 |
|
|
1783
1783
|
|---|---|
|
|
1784
|
-
| `ojm market setup [--url <站点>] [--user <u> --pass <p>] [--new-pass <p>]` | **外部真 CDN 场景才需要**:把 `framework.market_cdn_url` 改写为绝对地址;账号须 **superadmin**(该设置安全组仅 superadmin 可写,admin 角色会被 403);首跑若账号处于首次改密期会先自动改密再写入 |
|
|
1784
|
+
| `ojm market setup [--site-url <站点>] [--user <u> --pass <p>] [--new-pass <p>]` | **外部真 CDN 场景才需要**:把 `framework.market_cdn_url` 改写为绝对地址;账号须 **superadmin**(该设置安全组仅 superadmin 可写,admin 角色会被 403);首跑若账号处于首次改密期会先自动改密再写入 |
|
|
1785
1785
|
| `ojm market publish [--fixture <name>@<version>] [--real <方案工程>] [--id <key>] [--key <pem>] [--rebuild-index] [--check]` | 把方案包落进 `cdn/` 并做包级 detached 签名;**不碰索引**(索引由 `build-index` 重建,避免多人发布互相覆盖) |
|
|
1786
1786
|
| `ojm market build-index [--id <key>] [--key <pem>] [--check]` | 扫描 `cdn/*.tgz`、提取包内 manifest,全量重建 `bundles.yaml` + `manifests.yaml` 并签名;结果只取决于目录内容,连续执行字节一致 |
|
|
1787
1787
|
| `ojm market verify [--file <tgz>] [--rebuild-index]` | 离线验包(不起站点、不装、不要凭据):sha256 完整性 / 包级 Ed25519 验签 / 索引验签 / manifest 结构分列输出;`--rebuild-index` 额外比对重建索引与盘上索引的漂移 |
|
|
@@ -1908,9 +1908,9 @@ flowchart TD
|
|
|
1908
1908
|
`install` / `enable` / `disable` / `uninstall` / `trust` 都是**瘦客户端**——它们把请求发到目标站点
|
|
1909
1909
|
(需 superadmin),本地只做「登录 → 提交 → 轮询任务至终态」并打印结果。三处寻址约定完全一致:
|
|
1910
1910
|
|
|
1911
|
-
- `--url <站点地址>`:缺省 `http://127.0.0.1:9778`;
|
|
1911
|
+
- `--site-url <站点地址>`:缺省 `http://127.0.0.1:9778`;
|
|
1912
1912
|
- `--user <账号>` / `--pass <密码>`:站点凭据;
|
|
1913
|
-
- 等价环境变量 `
|
|
1913
|
+
- 等价环境变量 `OJM_SITE_URL` / `OJM_USER` / `OJM_PASS`(命令行缺省时回退读取,**凭据只走内存不落盘**);
|
|
1914
1914
|
- `--locale <zh-CN|en-US>`:`install` 专用,切换失败文案双语(默认按站点 locale)。
|
|
1915
1915
|
|
|
1916
1916
|
| 命令 | 作用 | 注意 |
|
|
@@ -1963,7 +1963,7 @@ ojm keygen --id <publisher>.<key-name>
|
|
|
1963
1963
|
ojm sign dist/<name>-<version>.tgz --id <publisher>.<key-name> --check
|
|
1964
1964
|
|
|
1965
1965
|
# ③ 安装/分发:.sig 自动探测随包上传
|
|
1966
|
-
ojm install dist/<name>-<version>.tgz --url <站点> --user superadmin --pass …
|
|
1966
|
+
ojm install dist/<name>-<version>.tgz --site-url <站点> --user superadmin --pass …
|
|
1967
1967
|
```
|
|
1968
1968
|
|
|
1969
1969
|
宿主验签六码(失败文案双语,`--locale` 切换):`sig_missing`(缺签)/ `sig_alg`(算法
|
|
@@ -165,7 +165,7 @@ node "$CLI_PKG/bin/ojm.mjs" market verify --rebuild-index
|
|
|
165
165
|
node "$CLI_PKG/bin/ojm.mjs" build # 生成 api/dist + web/dist(含 api/dist/versions.json)
|
|
166
166
|
# config:platform.root 改成 api/dist(安装目标恒为 release 部署,root 必须与 --api-path 同源)
|
|
167
167
|
(bin/oj serve -c config.yaml -b /api --api-path api/dist --console-log &)
|
|
168
|
-
node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user superadmin --pass 123456 --new-pass <新口令>
|
|
168
|
+
node "$CLI_PKG/bin/ojm.mjs" market setup --site-url http://127.0.0.1:9778 --user superadmin --pass 123456 --new-pass <新口令>
|
|
169
169
|
# 市场页点安装 → 首次停在「待信任」→ 核对指纹 → ojm trust confirm --task <id> → 重试 → done
|
|
170
170
|
```
|
|
171
171
|
|
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.43",
|
|
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.43",
|
|
53
53
|
"@redocly/cli": "^2.51.0",
|
|
54
54
|
"@tailwindcss/vite": "^4.3.0",
|
|
55
55
|
"cac": "^6.7.14",
|
|
@@ -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.43",
|
|
308
308
|
"license": "MIT"
|
|
309
309
|
},
|
|
310
|
-
"lastBuildTime": "2026-10-
|
|
310
|
+
"lastBuildTime": "2026-10-26 20:27:40"
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
var init_get_app_info = __esmMin((() => {}));
|
package/shell-dist/index.html
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
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-
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-1qa+1NwgUpqlsLbbr7v3iQ=='; 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
9
|
|
|
10
|
-
<script type="importmap" nonce="
|
|
10
|
+
<script type="importmap" nonce="1qa+1NwgUpqlsLbbr7v3iQ==">{
|
|
11
11
|
"imports": {
|
|
12
12
|
"react": "/assets/react.js",
|
|
13
13
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
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.43",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.43",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.43",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -23,6 +23,9 @@ const buildBundleMock = vi.hoisted(() => vi.fn(async () => [{ file: "x.tgz" }]))
|
|
|
23
23
|
vi.mock("../bundle", () => ({ buildBundle: buildBundleMock }));
|
|
24
24
|
const vendorCommandMock = vi.hoisted(() => vi.fn(async () => {}));
|
|
25
25
|
vi.mock("../vendor", () => ({ vendorCommand: vendorCommandMock }));
|
|
26
|
+
// REG-CS3 v2:sign 的 --registry-id 曾只被 signCommand 要求、却没在 CLI 上接线,命令成死路
|
|
27
|
+
const signCommandMock = vi.hoisted(() => vi.fn(() => ({})));
|
|
28
|
+
vi.mock("../sign", () => ({ signCommand: signCommandMock }));
|
|
26
29
|
|
|
27
30
|
beforeEach(() => {
|
|
28
31
|
logs.length = 0;
|
|
@@ -35,6 +38,7 @@ afterEach(() => {
|
|
|
35
38
|
vi.restoreAllMocks();
|
|
36
39
|
buildBundleMock.mockClear();
|
|
37
40
|
vendorCommandMock.mockClear();
|
|
41
|
+
signCommandMock.mockClear();
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
interface RunResult { out: string, unknown: string[], exitCode: number | undefined }
|
|
@@ -116,22 +120,22 @@ describe("反常识行为修正(20261023-0937)", () => {
|
|
|
116
120
|
expect(isKnownCommand("nope")).toBe(false);
|
|
117
121
|
});
|
|
118
122
|
|
|
119
|
-
it("unknownCommandResult:help 形态报未知命令(双语),code
|
|
123
|
+
it("unknownCommandResult:help 形态报未知命令(双语),code 2(用法错误)", async () => {
|
|
120
124
|
const { unknownCommandResult } = await import("../cli");
|
|
121
125
|
const zh = unknownCommandResult("nope", "zh-CN", true);
|
|
122
126
|
expect(zh.text).toContain("[ojm] 未知命令:nope");
|
|
123
|
-
expect(zh.code).toBe(
|
|
127
|
+
expect(zh.code).toBe(2);
|
|
124
128
|
const en = unknownCommandResult("nope", "en-US", true);
|
|
125
129
|
expect(en.text).toContain("[ojm] Unknown command: nope");
|
|
126
|
-
expect(en.code).toBe(
|
|
130
|
+
expect(en.code).toBe(2);
|
|
127
131
|
});
|
|
128
132
|
|
|
129
|
-
it("unknownCommandResult:非 help 形态给顶层帮助,code
|
|
133
|
+
it("unknownCommandResult:非 help 形态给顶层帮助,code 2(用法错误)", async () => {
|
|
130
134
|
const { unknownCommandResult } = await import("../cli");
|
|
131
135
|
const { usageText } = await import("../usage");
|
|
132
136
|
const r = unknownCommandResult("nope", "zh-CN", false);
|
|
133
137
|
expect(r.text).toBe(usageText("zh-CN"));
|
|
134
|
-
expect(r.code).toBe(
|
|
138
|
+
expect(r.code).toBe(2);
|
|
135
139
|
});
|
|
136
140
|
|
|
137
141
|
it("localizeCacError:未知选项本地化(双语)+ 指引", async () => {
|
|
@@ -152,28 +156,28 @@ describe("反常识行为修正(20261023-0937)", () => {
|
|
|
152
156
|
});
|
|
153
157
|
|
|
154
158
|
describe("校验搬入 action 后行为保持", () => {
|
|
155
|
-
it("new 多余位置参数显式拒绝(F8
|
|
159
|
+
it("new 多余位置参数显式拒绝(F8),exit 2", async () => {
|
|
156
160
|
const r = await run(["new", "crm", "erp"], "zh-CN", tmp);
|
|
157
161
|
expect(r.out).toContain("只接受一个方案名");
|
|
158
|
-
expect(r.exitCode).toBe(
|
|
162
|
+
expect(r.exitCode).toBe(2);
|
|
159
163
|
});
|
|
160
164
|
|
|
161
|
-
it("new --layout 非 container|none
|
|
165
|
+
it("new --layout 非 container|none 报错,exit 2", async () => {
|
|
162
166
|
const r = await run(["new", "demo", "--layout", "bogus"], "zh-CN", tmp);
|
|
163
167
|
expect(r.out).toContain("--layout 只接受 container | none");
|
|
164
|
-
expect(r.exitCode).toBe(
|
|
168
|
+
expect(r.exitCode).toBe(2);
|
|
165
169
|
});
|
|
166
170
|
|
|
167
|
-
it("new 缺 name 打印用法并 exit
|
|
171
|
+
it("new 缺 name 打印用法并 exit 2", async () => {
|
|
168
172
|
const r = await run(["new"], "zh-CN", tmp);
|
|
169
173
|
expect(r.out).toContain("ojm new <name>");
|
|
170
|
-
expect(r.exitCode).toBe(
|
|
174
|
+
expect(r.exitCode).toBe(2);
|
|
171
175
|
});
|
|
172
176
|
|
|
173
|
-
it("vendor 非法 tag
|
|
177
|
+
it("vendor 非法 tag 报错(exit 2:改命令行即可)", async () => {
|
|
174
178
|
const r = await run(["vendor", "notatag"], "zh-CN", tmp);
|
|
175
179
|
expect(r.out).toContain("非法版本号");
|
|
176
|
-
expect(r.exitCode).toBe(
|
|
180
|
+
expect(r.exitCode).toBe(2);
|
|
177
181
|
});
|
|
178
182
|
|
|
179
183
|
it("vendor 0.1.12 规范化补 v 前缀后传入 vendorCommand", async () => {
|
|
@@ -188,7 +192,7 @@ describe("校验搬入 action 后行为保持", () => {
|
|
|
188
192
|
});
|
|
189
193
|
|
|
190
194
|
describe("参数映射", () => {
|
|
191
|
-
it("merge <out> <inputs...> variadic
|
|
195
|
+
it("merge <out> <inputs...> variadic 映射(缺文件报错提及输入名,exit 1:业务失败)", async () => {
|
|
192
196
|
const r = await run(["merge", "out.json", "in1.json", "in2.json"], "zh-CN", tmp);
|
|
193
197
|
expect(r.out).toContain("in1.json");
|
|
194
198
|
expect(r.exitCode).toBe(1);
|
|
@@ -204,6 +208,11 @@ describe("参数映射", () => {
|
|
|
204
208
|
await run(["build", "--bundle"], "zh-CN", tmp, () => buildBundleMock.mock.calls.length > 0);
|
|
205
209
|
expect(buildBundleMock).toHaveBeenCalledWith(tmp, { out: undefined, name: undefined });
|
|
206
210
|
});
|
|
211
|
+
|
|
212
|
+
it("sign --registry-id 映射到 signCommand.registryId(REG-CS3 v2 规范串首段)", async () => {
|
|
213
|
+
await run(["sign", "demo-0.1.0.tgz", "--id", "acme.market", "--registry-id", "oj-registry"], "zh-CN", tmp, () => signCommandMock.mock.calls.length > 0);
|
|
214
|
+
expect(signCommandMock).toHaveBeenCalledWith("demo-0.1.0.tgz", { id: "acme.market", key: undefined, check: undefined, registryId: "oj-registry" });
|
|
215
|
+
});
|
|
207
216
|
});
|
|
208
217
|
|
|
209
218
|
describe("help 词形", () => {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// BDD:`ojm list`(站点已装方案的可查面)。
|
|
2
|
+
//
|
|
3
|
+
// 到 0.3.2 为止 CLI 没有任何命令能回答「这个站点上装了什么」——装完只有一条终态文案。
|
|
4
|
+
// 用例盯四件事:
|
|
5
|
+
// ① 缺省只列**已装**(installed != null),`--all` 才连未装行一起列;
|
|
6
|
+
// ② 三个派生结论不许算错:upgradable / enabled / trust_state 都原样取自站点索引行;
|
|
7
|
+
// ③ 空清算是成功(exit 0),并在人话里给出下一步(`ojm list --all`);
|
|
8
|
+
// ④ `--json` 下 stdout 只有一行信封,命令名 `list`,数据在 data.solutions。
|
|
9
|
+
import type { SolutionRow } from "../list";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { buildCli, localizeCacError, reportCliError } from "../cli";
|
|
13
|
+
import { listCommand } from "../list";
|
|
14
|
+
|
|
15
|
+
/** fetch stub:按 URL 前缀分发,记录全部调用(uninstall.test.ts 同范式) */
|
|
16
|
+
function fetchStub(handlers: Record<string, (url: string) => unknown>): { fetchFn: typeof fetch, calls: string[] } {
|
|
17
|
+
const calls: string[] = [];
|
|
18
|
+
const fetchFn = (async (url: string | URL) => {
|
|
19
|
+
const u = String(url);
|
|
20
|
+
calls.push(u);
|
|
21
|
+
for (const [prefix, handler] of Object.entries(handlers)) {
|
|
22
|
+
if (u.includes(prefix))
|
|
23
|
+
return handler(u);
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`fetchStub 未覆盖:${u}`);
|
|
26
|
+
}) as typeof fetch;
|
|
27
|
+
return { fetchFn, calls };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ok = (data: unknown) => ({ ok: true, status: 200, text: async () => JSON.stringify({ code: 0, data }) });
|
|
31
|
+
const LOGIN = () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 });
|
|
32
|
+
|
|
33
|
+
const ROWS = [
|
|
34
|
+
{ name: "demo-solution", latest: "0.1.2", installed: "0.1.0", enabled: true, upgradable: true, available: ["0.1.0", "0.1.2"], trust_state: "trusted" },
|
|
35
|
+
{ name: "acme-demo", latest: "2.0.0", installed: "2.0.0", enabled: false, upgradable: false, available: ["2.0.0"], trust_state: "untrusted" },
|
|
36
|
+
{ name: "not-installed-one", latest: "1.0.0", installed: null, enabled: null, upgradable: false, available: ["1.0.0"], trust_state: null },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
let logs: string[];
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
logs = [];
|
|
43
|
+
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
44
|
+
logs.push(args.map(String).join(" "));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("listCommand:已装清单", () => {
|
|
53
|
+
it("scenario: 缺省只列已装,按名字排序,upgradable / trust_state 原样透出", async () => {
|
|
54
|
+
const { fetchFn, calls } = fetchStub({
|
|
55
|
+
"/api/auth/login": LOGIN,
|
|
56
|
+
"/api/_platform/market/index": () => ok(ROWS),
|
|
57
|
+
});
|
|
58
|
+
const rows = await listCommand({ siteUrl: "http://x", user: "u", pass: "p", fetchFn });
|
|
59
|
+
|
|
60
|
+
expect(rows.map(r => r.name)).toEqual(["acme-demo", "demo-solution"]); // 未装的不出现
|
|
61
|
+
expect(rows.find(r => r.name === "demo-solution")).toEqual({
|
|
62
|
+
name: "demo-solution",
|
|
63
|
+
installed: "0.1.0",
|
|
64
|
+
enabled: true,
|
|
65
|
+
latest: "0.1.2",
|
|
66
|
+
upgradable: true,
|
|
67
|
+
trustState: "trusted",
|
|
68
|
+
});
|
|
69
|
+
// 登录用的是站点寻址(Bearer 由 callApi 注入,这里只断言两次调用与顺序)
|
|
70
|
+
expect(calls).toEqual(["http://x/api/auth/login", "http://x/api/_platform/market/index"]);
|
|
71
|
+
|
|
72
|
+
const text = logs.join("\n");
|
|
73
|
+
expect(text).toContain("已装 2 个方案");
|
|
74
|
+
expect(text).toContain("NAME");
|
|
75
|
+
expect(text).toContain("可升级:demo-solution");
|
|
76
|
+
expect(text).toContain("签名不受信:acme-demo");
|
|
77
|
+
expect(text).toContain("no"); // 禁用态
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("scenario: --all 连未装行一起列(installed 显示 -),人话里给两个计数", async () => {
|
|
81
|
+
const { fetchFn } = fetchStub({
|
|
82
|
+
"/api/auth/login": LOGIN,
|
|
83
|
+
"/api/_platform/market/index": () => ok(ROWS),
|
|
84
|
+
});
|
|
85
|
+
const rows = await listCommand({ siteUrl: "http://x", user: "u", pass: "p", fetchFn, all: true });
|
|
86
|
+
|
|
87
|
+
expect(rows.map(r => r.name)).toEqual(["acme-demo", "demo-solution", "not-installed-one"]);
|
|
88
|
+
expect(rows.find(r => r.name === "not-installed-one")).toMatchObject({ installed: null, upgradable: false, enabled: null });
|
|
89
|
+
const text = logs.join("\n");
|
|
90
|
+
expect(text).toContain("3 个方案(已装 2 个)");
|
|
91
|
+
expect(text).toMatch(/not-installed-one\s+-/); // 未装的行 installed 列是 `-`
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("scenario: 站点上一个方案都没装 → 成功(exit 0 语义),人话指路 --all", async () => {
|
|
95
|
+
const { fetchFn } = fetchStub({
|
|
96
|
+
"/api/auth/login": LOGIN,
|
|
97
|
+
"/api/_platform/market/index": () => ok([ROWS[2]]),
|
|
98
|
+
});
|
|
99
|
+
const rows = await listCommand({ siteUrl: "http://x", user: "u", pass: "p", fetchFn });
|
|
100
|
+
expect(rows).toEqual([]);
|
|
101
|
+
expect(logs.join("\n")).toContain("还没有已装方案");
|
|
102
|
+
expect(logs.join("\n")).toContain("ojm list --all");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("scenario: 缺凭据 → 用法错误(2),不去联网", async () => {
|
|
106
|
+
const { fetchFn, calls } = fetchStub({});
|
|
107
|
+
await expect(listCommand({ siteUrl: "http://x", fetchFn })).rejects.toThrowError(/缺少站点凭据/);
|
|
108
|
+
expect(calls).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/* ────────────────────────────── CLI 接线与信封 ────────────────────────────── */
|
|
113
|
+
|
|
114
|
+
interface RunResult { out: string[], err: string[], exitCode: number | undefined }
|
|
115
|
+
|
|
116
|
+
async function run(argv: string[], fetchFn: typeof fetch): Promise<RunResult> {
|
|
117
|
+
vi.stubGlobal("fetch", fetchFn);
|
|
118
|
+
const out: string[] = [];
|
|
119
|
+
const err: string[] = [];
|
|
120
|
+
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
121
|
+
out.push(args.map(String).join(" "));
|
|
122
|
+
});
|
|
123
|
+
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
|
124
|
+
err.push(args.map(String).join(" "));
|
|
125
|
+
});
|
|
126
|
+
const exitSpy = vi.fn();
|
|
127
|
+
const exit = exitSpy as unknown as (code: number) => never;
|
|
128
|
+
const cli = buildCli(process.cwd(), "zh-CN", { exit, argv });
|
|
129
|
+
try {
|
|
130
|
+
cli.parse(["node", "ojm", ...argv], { run: true });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
reportCliError(localizeCacError(error, "zh-CN"), exit, argv);
|
|
134
|
+
}
|
|
135
|
+
await vi.waitUntil(() => out.length > 0 || err.length > 0 || exitSpy.mock.calls.length > 0, { timeout: 3000, interval: 10 });
|
|
136
|
+
return { out, err, exitCode: exitSpy.mock.calls[0]?.[0] as number | undefined };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
describe("list:CLI 接线与信封", () => {
|
|
140
|
+
it("scenario: --json → stdout 只有一行信封,data.solutions 是过滤后的已装行", async () => {
|
|
141
|
+
const { fetchFn } = fetchStub({
|
|
142
|
+
"/api/auth/login": LOGIN,
|
|
143
|
+
"/api/_platform/market/index": () => ok(ROWS),
|
|
144
|
+
});
|
|
145
|
+
const r = await run(["list", "--site-url", "http://x", "--user", "u", "--pass", "p", "--json"], fetchFn);
|
|
146
|
+
|
|
147
|
+
expect(r.exitCode).toBeUndefined();
|
|
148
|
+
expect(r.out).toHaveLength(1);
|
|
149
|
+
const envelope = JSON.parse(r.out[0]!) as { ok: boolean, command: string, code: string, data: { site: string, all: boolean, solutions: SolutionRow[] } };
|
|
150
|
+
expect(envelope).toMatchObject({ ok: true, command: "list", code: "ok" });
|
|
151
|
+
expect(envelope.data.site).toBe("http://x");
|
|
152
|
+
expect(envelope.data.all).toBe(false);
|
|
153
|
+
expect(envelope.data.solutions.map(s => s.name)).toEqual(["acme-demo", "demo-solution"]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("scenario: --all 也进信封(all = true 且包含未装行)", async () => {
|
|
157
|
+
const { fetchFn } = fetchStub({
|
|
158
|
+
"/api/auth/login": LOGIN,
|
|
159
|
+
"/api/_platform/market/index": () => ok(ROWS),
|
|
160
|
+
});
|
|
161
|
+
const r = await run(["list", "--site-url", "http://x", "--user", "u", "--pass", "p", "--all", "--json"], fetchFn);
|
|
162
|
+
const envelope = JSON.parse(r.out[0]!) as { data: { all: boolean, solutions: SolutionRow[] } };
|
|
163
|
+
expect(envelope.data.all).toBe(true);
|
|
164
|
+
expect(envelope.data.solutions).toHaveLength(3);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("scenario: 站点报错(索引 503)→ 业务失败(1),错误话说清楚", async () => {
|
|
168
|
+
const { fetchFn } = fetchStub({
|
|
169
|
+
"/api/auth/login": LOGIN,
|
|
170
|
+
"/api/_platform/market/index": () => ({ ok: false, status: 503, text: async () => JSON.stringify({ code: 503, msg: "未配置市场地址" }) }),
|
|
171
|
+
});
|
|
172
|
+
const r = await run(["list", "--site-url", "http://x", "--user", "u", "--pass", "p"], fetchFn);
|
|
173
|
+
expect(r.exitCode).toBe(1);
|
|
174
|
+
expect(r.err.join("\n")).toContain("未配置市场地址");
|
|
175
|
+
});
|
|
176
|
+
});
|