@oj-module/cli 0.2.20 → 0.2.25
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 +100 -12
- package/devkit/module-dev-and-cdn-playground.md +10 -10
- package/package.json +4 -4
- package/shell-dist/assets/runtime.js +61 -35
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +13 -13
- package/src/__tests__/cli.test.ts +225 -0
- package/src/__tests__/init-packagejson.test.ts +29 -0
- package/src/__tests__/layout.test.ts +8 -5
- package/src/__tests__/new-scaffold.test.ts +59 -0
- package/src/__tests__/usage.test.ts +45 -47
- package/src/build.ts +4 -4
- package/src/builtin.ts +93 -0
- package/src/bundle.ts +237 -33
- package/src/cli.ts +265 -0
- package/src/contract/__tests__/market-client.integration.test.ts +12 -12
- package/src/contract/check.ts +4 -4
- package/src/dev.ts +75 -2
- package/src/index.ts +31 -191
- package/src/info.ts +49 -7
- package/src/init.ts +25 -11
- package/src/keygen.ts +0 -19
- package/src/layout.ts +11 -11
- package/src/lifecycle/__tests__/authoring.test.ts +3 -14
- package/src/lifecycle/__tests__/builtin.test.ts +97 -0
- package/src/lifecycle/__tests__/bundle.test.ts +188 -25
- package/src/lifecycle/__tests__/install.test.ts +0 -23
- package/src/lifecycle/__tests__/new.test.ts +129 -0
- package/src/lifecycle/__tests__/smoke.test.ts +4 -3
- package/src/manifest-contract.ts +16 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -0
- package/src/market/__tests__/setup.test.ts +8 -1
- package/src/market/bootstrap.ts +10 -8
- package/src/market/cdn-layout.ts +3 -3
- package/src/market/command.ts +0 -23
- package/src/market/publish.ts +10 -2
- package/src/market/setup.ts +14 -2
- package/src/market/trial.ts +3 -3
- package/src/market/walkthrough.ts +7 -6
- package/src/new.ts +226 -0
- package/src/preview-config.ts +2 -2
- package/src/preview.ts +5 -4
- package/src/publish.ts +0 -22
- package/src/sign.ts +0 -22
- package/src/usage.ts +482 -198
- package/templates/README.md +3 -3
- package/templates/cdn/README.md +6 -6
- package/templates/config.yaml +5 -4
- package/templates/init.include.yaml +1 -1
- package/templates/package.json +3 -1
- package/templates/seed.mysql.sql +2 -1
- package/templates/seed.pg.sql +2 -1
- package/templates/src/_platform/api/fixtures/l1-test-user.sql +4 -5
- package/templates/src/_platform/api/market/api.ts +11 -8
- package/templates/src/_platform/api/seed.mysql.sql +11 -3
- package/templates/src/_platform/api/seed.pg.sql +11 -3
- package/templates/src/_platform/api/seed.sql +18 -6
- package/templates/src/_platform/api/solutions/api.ts +2 -2
- package/templates/src/oj-market/web/api/context.ts +2 -2
- package/templates/src/oj-market/web/components/InstallDialog.tsx +13 -6
- package/templates/src/oj-market/web/components/RollbackDialog.tsx +7 -3
- package/templates/src/oj-market/web/components/UninstallConfirm.tsx +18 -7
- package/templates/src/oj-market/web/hooks/useMarket.ts +30 -18
- package/templates/src/system/web/locales/en-US.json +1 -1
- package/templates/src/system/web/locales/zh-CN.json +1 -1
- package/templates/src/system/web/pages/users/roles.tsx +3 -1
- package/templates/tests/l1-builder.test.ts +2 -2
- package/templates/tests/lifecycle/market-api.spec.ts +35 -5
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/lifecycle/verify-report.spec.ts +1 -1
- package/templates/tests/rbac-seed.spec.ts +12 -10
- package/templates/tests/seed-role-accounts.spec.ts +87 -0
- package/vendor/host-versions.json +3 -3
- package/src/args.ts +0 -122
|
@@ -331,6 +331,36 @@ describe("POST install(cdn:Form B 索引解析属市场侧,安装器不拼
|
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
describe("相对基址(feature:/cdn = 本站点自供,按请求源补全同源绝对 URL)", () => {
|
|
335
|
+
const REL = "/cdn";
|
|
336
|
+
const HOST = "127.0.0.1:9778";
|
|
337
|
+
const relEnv = () => makeEnv({ cdnBase: async () => REL });
|
|
338
|
+
|
|
339
|
+
it("index:索引与 .sig/manifests 拉取按请求 Host 补全为同源绝对 URL", async () => {
|
|
340
|
+
const api = createMarketApi(relEnv());
|
|
341
|
+
g.http = { user: { id: 1 }, headers: { host: HOST } };
|
|
342
|
+
await api.index.get();
|
|
343
|
+
expect(last?.status).toBe(200);
|
|
344
|
+
expect(fetchCalls).toContain(`http://${HOST}/cdn/bundles.yaml`);
|
|
345
|
+
expect(fetchCalls).toContain(`http://${HOST}/cdn/manifests.yaml`);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("install:archive 分发包同样补全同源绝对 URL", async () => {
|
|
349
|
+
const api = createMarketApi(relEnv());
|
|
350
|
+
g.http = { user: { id: 1 }, body: { name: "demo-solution", version: "0.1.0", source: "cdn" }, headers: { host: HOST } };
|
|
351
|
+
await api.install.post();
|
|
352
|
+
expect(last?.status).toBe(200);
|
|
353
|
+
expect(fetchCalls).toContain(`http://${HOST}/cdn/demo-solution-0.1.0.tgz`);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("无请求上下文(无 Host):不猜原点,原样透传相对 URL", async () => {
|
|
357
|
+
const api = createMarketApi(relEnv());
|
|
358
|
+
g.http = { user: { id: 1 } };
|
|
359
|
+
await api.index.get();
|
|
360
|
+
expect(fetchCalls).toContain("/cdn/bundles.yaml");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
334
364
|
describe("POST install/cancel(P3-12 端点接线,U5/P3-8 令牌的消费面)", () => {
|
|
335
365
|
it("无进行中安装 → 404(取消只对 running 态有效,任务终态后信号已注销)", async () => {
|
|
336
366
|
const api = createMarketApi(makeEnv());
|
|
@@ -858,12 +888,12 @@ describe("hostVersions 缺省接线(B-11:C2-09 比对真源 = <dist>/version
|
|
|
858
888
|
});
|
|
859
889
|
|
|
860
890
|
describe("P1-16 接线:marketEnv 生产口径 / exec opts 转发 / operator 通道 / 账本真源 / index 即对账入口", () => {
|
|
861
|
-
it("marketEnv() 缺省 = 布局
|
|
891
|
+
it("marketEnv() 缺省 = 布局 v3 生产口径:版本目录与 versions.json 在 release 根 dist/api,站点 modules 在 dist/web;暂存/任务/sidecar 留 jail 根", () => {
|
|
862
892
|
const env = marketEnv();
|
|
863
|
-
expect(env.sharedRoot).toBe("api
|
|
864
|
-
expect(env.siteModulesDir).toBe("web/
|
|
865
|
-
expect(env.modulesJsonPath).toBe("web/
|
|
866
|
-
expect(versionsJsonPathOf(env.sharedRoot)).toBe("api/
|
|
893
|
+
expect(env.sharedRoot).toBe("dist/api");
|
|
894
|
+
expect(env.siteModulesDir).toBe("dist/web/modules");
|
|
895
|
+
expect(env.modulesJsonPath).toBe("dist/web/modules.json");
|
|
896
|
+
expect(versionsJsonPathOf(env.sharedRoot)).toBe("dist/api/versions.json");
|
|
867
897
|
expect(env.staging).toBe("modules/.staging");
|
|
868
898
|
expect(env.taskDir).toBe("modules/.install-tasks");
|
|
869
899
|
expect(env.statePath).toBe("modules/.solutions/state.yaml");
|
|
@@ -278,7 +278,7 @@ function setHttp(over: Record<string, unknown>) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
function makeEnv() {
|
|
281
|
-
// modulesJsonPath 钉进临时目录(readFile 失败 → 不打 web 项):默认值 `web/
|
|
281
|
+
// modulesJsonPath 钉进临时目录(readFile 失败 → 不打 web 项):默认值 `dist/web/modules.json`
|
|
282
282
|
// 是相对 cwd 的,会把模板本机构建产物卷进用例,全绿用例随「模板 build 过没有」摇摆。
|
|
283
283
|
// P4-2 两条 integrity 用例显式覆盖它。
|
|
284
284
|
return { ...solutionsEnv(), sharedRoot: root, statePath: join(root, "state.yaml"), modulesJsonPath: join(root, "modules.json") };
|
|
@@ -128,7 +128,7 @@ function setHttp(over: Record<string, unknown> = {}) {
|
|
|
128
128
|
|
|
129
129
|
function makeEnv() {
|
|
130
130
|
// modulesJsonPath 同 verify-checks.spec:钉进临时目录(缺文件 → 跳过 web 项),
|
|
131
|
-
// 不吃模板本机 `web/
|
|
131
|
+
// 不吃模板本机 `dist/web/modules.json`。
|
|
132
132
|
return { ...solutionsEnv(), sharedRoot: root, statePath: join(root, "state.yaml"), modulesJsonPath: join(root, "modules.json"), verifyReportsDir: reportsDir };
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
// BDD:seed 契约——框架内置角色 +
|
|
4
|
+
// BDD:seed 契约——框架内置角色 + 内置角色开发账号(账号名=角色名,must_change=0,
|
|
5
|
+
// 2026-10-22 口径,替代原「admin 绑 superadmin + must_change=1」的 D12 旧钉)。
|
|
5
6
|
// 夹具重放真实 migrations + seed.sql 文件,本 spec 即「seed 与文件不漂移」的钉子。
|
|
6
7
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
7
8
|
|
|
@@ -38,17 +39,18 @@ describe("框架内置角色(ROLES 常量单一真相,PRD §4.2)", () => {
|
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
describe("seed
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
it("admin 账号绑定 superadmin 角色(sys_user_role 权威源)", () => {
|
|
46
|
-
const row = dbo.prepare("SELECT * FROM sys_user_role WHERE user_id = ? AND role_name = 'superadmin'").get(adminId());
|
|
42
|
+
describe("seed 内置角色开发账号(账号名=角色名,2026-10-22 口径)", () => {
|
|
43
|
+
it.each(["superadmin", "admin", "editor", "viewer"])("账号 %s 绑定同名角色且 must_change=0", (name) => {
|
|
44
|
+
const uid = (dbo.prepare("SELECT id FROM users WHERE username = ?").get(name) as { id: number }).id;
|
|
45
|
+
const row = dbo.prepare("SELECT * FROM sys_user_role WHERE user_id = ? AND role_name = ?").get(uid, name);
|
|
47
46
|
expect(row).toBeDefined();
|
|
47
|
+
const mc = dbo.prepare("SELECT must_change FROM users WHERE id = ?").get(uid) as { must_change: number };
|
|
48
|
+
expect(Number(mc.must_change)).toBe(0);
|
|
48
49
|
});
|
|
49
50
|
|
|
50
|
-
it("
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
it("admin 不再绑 superadmin(旧 D12 绑定已被幂等 DELETE 收敛)", () => {
|
|
52
|
+
const uid = (dbo.prepare("SELECT id FROM users WHERE username = 'admin'").get() as { id: number }).id;
|
|
53
|
+
const row = dbo.prepare("SELECT * FROM sys_user_role WHERE user_id = ? AND role_name = 'superadmin'").get(uid);
|
|
54
|
+
expect(row).toBeUndefined();
|
|
53
55
|
});
|
|
54
56
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// BDD:脚手架 seed 内置角色开发账号——账号名=角色名一一对应(superadmin/admin/editor/viewer),
|
|
2
|
+
// 口令均 123456,must_change=0 免首登改密(纯开发便利,生产部署须改密/删除)。
|
|
3
|
+
// 旧库收敛:历史 seed 把 admin 绑在 superadmin 且 must_change=1,seed 幂等重放即收敛
|
|
4
|
+
// (DELETE 旧绑定 + UPDATE must_change=0),无需手工迁移。
|
|
5
|
+
// 直接重放 _platform 迁移 + seed.sql 到内存 sqlite(installDb 不 replay seed,故自给自足)。
|
|
6
|
+
// 方言对齐:seed.mysql.sql / seed.pg.sql 与基线同构,人工对齐不受本 spec 钉守——
|
|
7
|
+
// 首条用例对三份文件做 INSERT 语法冒烟,防「INSERT IGNORE 错位」类事故回归。
|
|
8
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
9
|
+
|
|
10
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
const req = createRequire(import.meta.url);
|
|
17
|
+
const { DatabaseSync: Db } = req("node:sqlite") as { DatabaseSync: typeof DatabaseSync };
|
|
18
|
+
|
|
19
|
+
const PLATFORM_API = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "_platform", "api");
|
|
20
|
+
|
|
21
|
+
let dbo: InstanceType<typeof DatabaseSync>;
|
|
22
|
+
|
|
23
|
+
/** 重放迁移 + seed(seed 按英文分号朴素切分,同 oj 运行时口径) */
|
|
24
|
+
function replaySeed(): void {
|
|
25
|
+
for (const stmt of readFileSync(join(PLATFORM_API, "seed.sql"), "utf8").split(";")) {
|
|
26
|
+
const sql = stmt.trim();
|
|
27
|
+
if (sql)
|
|
28
|
+
dbo.exec(sql);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
dbo = new Db(":memory:");
|
|
34
|
+
for (const f of readdirSync(join(PLATFORM_API, "migrations")).filter(f => f.endsWith(".sql") && !/\.(?:mysql|pg)\.sql$/.test(f)).sort())
|
|
35
|
+
dbo.exec(readFileSync(join(PLATFORM_API, "migrations", f), "utf8"));
|
|
36
|
+
replaySeed();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("seed 内置角色开发账号", () => {
|
|
40
|
+
it("方言文件 INSERT 语法冒烟:mysql 为 INSERT IGNORE 前缀、pg 为 ON CONFLICT 后缀", () => {
|
|
41
|
+
const mysql = readFileSync(join(PLATFORM_API, "seed.mysql.sql"), "utf8");
|
|
42
|
+
const pg = readFileSync(join(PLATFORM_API, "seed.pg.sql"), "utf8");
|
|
43
|
+
expect(mysql).not.toMatch(/INSERT INTO .*INSERT IGNORE/);
|
|
44
|
+
expect(mysql.match(/INSERT IGNORE INTO/g)!.length).toBeGreaterThanOrEqual(12);
|
|
45
|
+
expect(pg).not.toMatch(/INSERT IGNORE/);
|
|
46
|
+
expect(pg.match(/ON CONFLICT DO NOTHING/g)!.length).toBeGreaterThanOrEqual(12);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each(["superadmin", "admin", "editor", "viewer"])("账号 %s:must_change=0 且只绑定同名角色", (username) => {
|
|
50
|
+
const row = dbo.prepare("SELECT must_change FROM users WHERE username = ?").get(username) as { must_change: number };
|
|
51
|
+
expect(row).toBeDefined();
|
|
52
|
+
expect(Number(row.must_change)).toBe(0);
|
|
53
|
+
const roles = dbo.prepare("SELECT role_name FROM sys_user_role WHERE user_id = (SELECT id FROM users WHERE username = ?)").all(username) as Array<{ role_name: string }>;
|
|
54
|
+
expect(roles.map(r => r.role_name)).toEqual([username]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("四个角色账号口令一致(123456 的 bcrypt,复用 seed 既有哈希)", () => {
|
|
58
|
+
const hashes = dbo.prepare("SELECT username, password_hash h FROM users WHERE username IN ('superadmin','admin','editor','viewer')").all() as Array<{ username: string, h: string }>;
|
|
59
|
+
expect(new Set(hashes.map(r => r.h)).size).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("旧库收敛:legacy admin(superadmin 绑定 + must_change=1) 经重放收敛为账号名=角色名", () => {
|
|
63
|
+
// 构造历史库状态:admin 绑 superadmin 且 must_change=1(旧 seed 口径),无 superadmin 账号
|
|
64
|
+
dbo.exec("DELETE FROM sys_user_role");
|
|
65
|
+
dbo.exec("DELETE FROM users");
|
|
66
|
+
dbo.prepare("INSERT INTO users (username, password_hash, must_change) VALUES ('admin', 'x', 1)").run();
|
|
67
|
+
dbo.prepare("INSERT INTO sys_user_role (user_id, role_name) SELECT id, 'superadmin' FROM users WHERE username = 'admin'").run();
|
|
68
|
+
|
|
69
|
+
replaySeed();
|
|
70
|
+
|
|
71
|
+
expect(Number((dbo.prepare("SELECT must_change m FROM users WHERE username = 'admin'").get() as { m: number }).m)).toBe(0);
|
|
72
|
+
const adminRoles = (dbo.prepare("SELECT role_name r FROM sys_user_role WHERE user_id = (SELECT id FROM users WHERE username = 'admin')").all() as Array<{ r: string }>).map(x => x.r);
|
|
73
|
+
expect(adminRoles).toEqual(["admin"]);
|
|
74
|
+
const superRoles = (dbo.prepare("SELECT role_name r FROM sys_user_role WHERE user_id = (SELECT id FROM users WHERE username = 'superadmin')").all() as Array<{ r: string }>).map(x => x.r);
|
|
75
|
+
expect(superRoles).toEqual(["superadmin"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("幂等:重放两遍结果不变", () => {
|
|
79
|
+
const snap = () => JSON.stringify([
|
|
80
|
+
dbo.prepare("SELECT username, must_change FROM users ORDER BY username").all(),
|
|
81
|
+
dbo.prepare("SELECT user_id, role_name FROM sys_user_role ORDER BY user_id, role_name").all(),
|
|
82
|
+
]);
|
|
83
|
+
const before = snap();
|
|
84
|
+
replaySeed();
|
|
85
|
+
expect(snap()).toBe(before);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -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.25",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.25",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.25",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|
package/src/args.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 子命令参数解析(纯函数,可测)。
|
|
3
|
-
*
|
|
4
|
-
* 约定:位置参数取第一个不以 `--` 开头的参数;`--xxx` 一律是开关/选项。
|
|
5
|
-
* 缺陷背景:`ojm init --yes` 曾把 argv[3](即 `--yes`)当目标目录。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export function parseInitArgs(argv: string[]): { dest: string, yes: boolean } {
|
|
9
|
-
const yes = argv.includes("--yes");
|
|
10
|
-
const dest = argv.find(a => !a.startsWith("--")) ?? "";
|
|
11
|
-
return { dest, yes };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* `ojm api [dir] [--check] [--docs] [--exempt <path>]`:位置参数为项目目录
|
|
16
|
-
* (缺省 cwd),与 parseInitArgs 同一约定(docs/prd/202609032019-ojm-api-cwd.md)。
|
|
17
|
-
* 注意:`--exempt` 的「值」不得以 `--` 开头,否则会被误判为 dir——故逐项扫描,
|
|
18
|
-
* 消费掉 `--exempt` 紧随的值,避免被 dir 解析吞噬(P6 同款坑)。
|
|
19
|
-
*/
|
|
20
|
-
export function parseApiArgs(argv: string[]): { dir: string, check: boolean, docs: boolean, exempt?: string } {
|
|
21
|
-
const flags = new Set<string>();
|
|
22
|
-
const positionals: string[] = [];
|
|
23
|
-
let exempt: string | undefined;
|
|
24
|
-
for (let i = 0; i < argv.length; i++) {
|
|
25
|
-
const a = argv[i];
|
|
26
|
-
if (a === "--exempt") {
|
|
27
|
-
exempt = argv[i + 1];
|
|
28
|
-
i++; // 跳过紧随的值,不被当 dir
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (a.startsWith("--")) {
|
|
32
|
-
flags.add(a);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
positionals.push(a);
|
|
36
|
-
}
|
|
37
|
-
const dir = positionals[0] ?? "";
|
|
38
|
-
return { dir, check: flags.has("--check"), docs: flags.has("--docs"), exempt };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* `ojm build [--bundle] [--out <dir>]`。
|
|
43
|
-
* `--bundle`:方案包构建(manifest.yaml 契约校验 → 确定性 tgz + sha256),
|
|
44
|
-
* 与站点级 `ojm build`(后端 oj build + 模块产物合并)互斥分工(devkit 手册「打包与分发」)。
|
|
45
|
-
* `--out` 的值不得以 `--` 开头(同 parseApiArgs 的消费式扫描,P6 同款坑)。
|
|
46
|
-
*/
|
|
47
|
-
export function parseBuildArgs(argv: string[]): { bundle: boolean, out?: string } {
|
|
48
|
-
const flags = new Set<string>();
|
|
49
|
-
let out: string | undefined;
|
|
50
|
-
for (let i = 0; i < argv.length; i++) {
|
|
51
|
-
const a = argv[i];
|
|
52
|
-
if (a === "--out") {
|
|
53
|
-
out = argv[i + 1];
|
|
54
|
-
i++; // 消费紧随的值,不被当 flag
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (a.startsWith("--")) {
|
|
58
|
-
flags.add(a);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return { bundle: flags.has("--bundle"), out };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* `ojm vendor [tag] [--force]`:下载/更新 oj vendor(npm 包 @oj-bin/oj)。
|
|
66
|
-
* tag 缺省追 npm latest;形如 `v0.1.13` 或 `0.1.13`(自动补 v 前缀,装包时去掉)。
|
|
67
|
-
* 非法 tag 形状即报错(防误把目录当版本)。
|
|
68
|
-
*/
|
|
69
|
-
export function parseVendorArgs(argv: string[]): { force: boolean, tag?: string } {
|
|
70
|
-
const tag = argv.find(a => !a.startsWith("--") && a !== "--");
|
|
71
|
-
if (tag !== undefined && !/^v?\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(tag)) {
|
|
72
|
-
throw new Error(`[ojm] 非法版本号:${tag}(形如 v0.1.11 或 0.1.11)`);
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
force: argv.includes("--force"),
|
|
76
|
-
tag: tag === undefined ? undefined : (tag.startsWith("v") ? tag : `v${tag}`),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface LifecycleArgs {
|
|
81
|
-
positionals: string[]
|
|
82
|
-
url?: string
|
|
83
|
-
user?: string
|
|
84
|
-
pass?: string
|
|
85
|
-
locale?: string
|
|
86
|
-
name?: string
|
|
87
|
-
version?: string
|
|
88
|
-
task?: string
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* `ojm install|enable|disable` 共用解析:位置参数(目标/方案名)+ 值式 flag。
|
|
93
|
-
* 站点寻址三件套 --url/--user/--pass(环境变量 OJM_URL/OJM_USER/OJM_PASS 兜底,见 src/site.ts);
|
|
94
|
-
* install 专属 --name/--version(文件名身份覆盖)与 --locale(终态文案语言);
|
|
95
|
-
* trust 专属 --task(TOFU 确认目标任务 id,P2-3)。
|
|
96
|
-
* 值式 flag 逐项消费紧随的值,不被当位置参数(P6 同款坑,parseApiArgs 同纪律)。
|
|
97
|
-
*/
|
|
98
|
-
export function parseLifecycleArgs(argv: string[]): LifecycleArgs {
|
|
99
|
-
const valueFlags = new Set(["--url", "--user", "--pass", "--locale", "--name", "--version", "--task"]);
|
|
100
|
-
const values: Record<string, string> = {};
|
|
101
|
-
const positionals: string[] = [];
|
|
102
|
-
for (let i = 0; i < argv.length; i++) {
|
|
103
|
-
const a = argv[i]!;
|
|
104
|
-
if (valueFlags.has(a)) {
|
|
105
|
-
values[a] = argv[i + 1] ?? "";
|
|
106
|
-
i++; // 消费紧随的值
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (!a.startsWith("--"))
|
|
110
|
-
positionals.push(a);
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
positionals,
|
|
114
|
-
url: values["--url"],
|
|
115
|
-
user: values["--user"],
|
|
116
|
-
pass: values["--pass"],
|
|
117
|
-
locale: values["--locale"],
|
|
118
|
-
name: values["--name"],
|
|
119
|
-
version: values["--version"],
|
|
120
|
-
task: values["--task"],
|
|
121
|
-
};
|
|
122
|
-
}
|