@oj-module/cli 0.2.2 → 0.2.6
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 +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +189 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +1 -1
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +62 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +12 -0
- package/templates/web/src/system/__client/api.ts +25 -1
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// 迁移步骤(P1-7,十阶段 stage 5 migrating;D11 核心裁决:迁移成功后才允许后续落锁)。
|
|
2
|
+
// R14:迁移前置备份——sqlite 库 copy 为 <db>.bak-<ts>(fsFace.copyFile,本任务补面),无备份不迁移
|
|
3
|
+
// (fail-closed:备份不可用则不执行 oj migrate,库保持原状);完成后清理仅保留最近 3 份 .bak-*
|
|
4
|
+
// (按时间戳后缀数值比较;清理失败仅告警,多留备份无害,不阻断主流程)。
|
|
5
|
+
// S1-6 实测契约(oj 0.1.76):exec.run(["oj","migrate","--format","json","-d",dir,module])——"oj" 由
|
|
6
|
+
// oj 特判解析为当前进程 exe,platform.exec 白名单逐字匹配,argv 不得增删改;stdout 恰一行 JSON
|
|
7
|
+
// {"results":[{module,ok,applied:[{version,name}],error?}]}(装配期全局失败时顶层 error + results 空);
|
|
8
|
+
// 成败真源 = exit code(exit 0 = 全部成功),逐模块 continue-and-collect,一个模块失败不阻断其余。
|
|
9
|
+
// 失败语义:StageResult{ok:false} + 可诊断摘要(含原始 error/results 截断入 message)——本步绝不触碰
|
|
10
|
+
// 锁与 state(落锁在 P1-10 编排;release 门禁 M003 账本超前拒启 / M004 账本落后拒启要求锁与账本
|
|
11
|
+
// 永不半落)。rollbackGuard 纯函数:目标产物最大 seq < 账本 head = 降版部署,禁止(OJ-9 红线,
|
|
12
|
+
// M003 同源);seq 是迁移序号非版本号,直接数值比较(semver.ts 不适用)。
|
|
13
|
+
|
|
14
|
+
import type { FsFace } from "../atomic";
|
|
15
|
+
import type { ExecFace } from "./extract";
|
|
16
|
+
import type { InstallContext } from "./preflight";
|
|
17
|
+
import { fsFace } from "../atomic";
|
|
18
|
+
import { serializeLock } from "../lock";
|
|
19
|
+
import { copyTree } from "./deploy-web";
|
|
20
|
+
|
|
21
|
+
export interface MigrateContext extends InstallContext {
|
|
22
|
+
/** 安装暂存根(§2.9 解包产物;api/<name>-<version>/ 模块目录由此物化进迁移服务目录) */
|
|
23
|
+
staging: string
|
|
24
|
+
/** sqlite 库文件路径(R14 备份源;备份落同目录 <db>.bak-<ts>) */
|
|
25
|
+
dbPath: string
|
|
26
|
+
/** 注入式 exec 面(生产 = 平台 exec.run 包装;测试 stub 不真跑 oj,同 extract DIP) */
|
|
27
|
+
exec: ExecFace
|
|
28
|
+
/** 当前迁移账本 head(P1-10 对账产物);缺省跳过禁降版判定 */
|
|
29
|
+
ledgerHead?: number | bigint | null
|
|
30
|
+
/** 段内进度回调(P2-13):spawn 流式路径逐块上报 frac∈[0,1)(渐近 1 的单调分数);缺省不报 */
|
|
31
|
+
onProgress?: (frac: number) => void
|
|
32
|
+
/** 解析成功回调(P2-13):appliedSeq 捕获真源(installer 据此落 TaskRecord.appliedSeq);仅解析成功时调用 */
|
|
33
|
+
onMigrateOutput?: (output: MigrateOutput) => void
|
|
34
|
+
/** 取消安装令牌(U5,P3-8):spawn 流式路径逐块探测,命中即 h.kill() 提前终止;run 缓冲路径在返回前探测 */
|
|
35
|
+
cancel?: () => boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StageResult {
|
|
39
|
+
stage: "migrating"
|
|
40
|
+
ok: boolean
|
|
41
|
+
message: string
|
|
42
|
+
retryable?: boolean
|
|
43
|
+
/** 取消安装(U5,P3-8):本段被用户中止——运行器据其走清场+回退装前语义 */
|
|
44
|
+
cancelled?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MigrateApplied {
|
|
48
|
+
/** 迁移 seq(数值序号非版本号,rollbackGuard 数值比较的来源) */
|
|
49
|
+
version: number
|
|
50
|
+
name: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MigrateModuleResult {
|
|
54
|
+
module: string
|
|
55
|
+
ok: boolean
|
|
56
|
+
applied: MigrateApplied[]
|
|
57
|
+
/** 失败原因原文(S1-6:DSN 已脱敏;仅诊断用,不参与判定) */
|
|
58
|
+
error?: unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MigrateOutput {
|
|
62
|
+
results: MigrateModuleResult[]
|
|
63
|
+
/** 装配期全局失败原因(S1-6:此时 results 为空;成败仍以 exit code 为真源) */
|
|
64
|
+
error?: unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** oj migrate 命令(逐字断言):与 platform.exec 白名单逐字对齐(S1-2/S1-6 基座),不得增删旗标 */
|
|
68
|
+
export function buildMigrateCmd(dir: string, module: string): string[] {
|
|
69
|
+
return ["oj", "migrate", "--format", "json", "-d", dir, module];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 迁移服务目录布局推导(B-11 核心):staging 的兄弟目录。D11 迁移先于落锁,模块尚不在站点锁内,
|
|
74
|
+
* sharedRoot 不可作 `-d`(oj release discover = load_lock 逐条目找 <dir>/<m>-<v>/,oj-bin
|
|
75
|
+
* manifest.rs 真源);svc 目录以「扁平锁 manifests.yaml + <name>-<version>/ 模块目录拷贝」
|
|
76
|
+
* 物化 release 形态,oj is_release 判定(目录含 manifests.yaml 即 release,serve_cmd.rs 真源)
|
|
77
|
+
* 使 dev 站点上同样走 release discover。`-migrate` 后缀不与任何 C2-01 合法名(版本目录名形态
|
|
78
|
+
* <name>-<version>)相撞——安装器仅在此目录上跑一次 migrate 即删,无共存面。
|
|
79
|
+
*/
|
|
80
|
+
export function migrateSvcDirOf(staging: string): string {
|
|
81
|
+
return `${staging}-migrate`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 解析 stdout 恰一行的 json 输出(纯函数,直测用)。严格形状校验:顶层键值对象 + results 数组 +
|
|
86
|
+
* 条目 {module:string, ok:boolean, applied:{version:number,name:string}[]};任何偏差都算契约破坏
|
|
87
|
+
* (fail-closed)。尾随换行/空白先 trim(stdout 恒带换行)。results 为空 + 顶层 error 是合法形态
|
|
88
|
+
* (装配期全局失败),成败判定交由 exit code。
|
|
89
|
+
*/
|
|
90
|
+
export function parseMigrateOutput(stdout: string): { ok: true, output: MigrateOutput } | { ok: false, message: string } {
|
|
91
|
+
let raw: unknown;
|
|
92
|
+
try {
|
|
93
|
+
raw = JSON.parse(stdout.trim());
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return { ok: false, message: `oj migrate stdout 不是合法 JSON(S1-6 契约:恰一行 {"results":[...]}):${String(e)}` };
|
|
97
|
+
}
|
|
98
|
+
if (!isObj(raw)) {
|
|
99
|
+
return { ok: false, message: `oj migrate stdout 顶层不是键值对象(实为 ${raw === null ? "null" : Array.isArray(raw) ? "数组" : typeof raw})` };
|
|
100
|
+
}
|
|
101
|
+
const resultsRaw = raw.results;
|
|
102
|
+
if (!Array.isArray(resultsRaw)) {
|
|
103
|
+
return { ok: false, message: "oj migrate stdout 缺少 results 数组(S1-6 契约)" };
|
|
104
|
+
}
|
|
105
|
+
const results: MigrateModuleResult[] = [];
|
|
106
|
+
for (const entry of resultsRaw) {
|
|
107
|
+
if (!isObj(entry) || typeof entry.module !== "string" || typeof entry.ok !== "boolean" || !Array.isArray(entry.applied)) {
|
|
108
|
+
return { ok: false, message: `oj migrate results 条目形状不符(须为 {module:string, ok:boolean, applied:array},实为 ${JSON.stringify(entry)?.slice(0, 200)})` };
|
|
109
|
+
}
|
|
110
|
+
const applied: MigrateApplied[] = [];
|
|
111
|
+
for (const a of entry.applied) {
|
|
112
|
+
if (!isObj(a) || typeof a.version !== "number" || typeof a.name !== "string") {
|
|
113
|
+
return { ok: false, message: `oj migrate「${entry.module}」applied 条目形状不符(须为 {version:number, name:string})` };
|
|
114
|
+
}
|
|
115
|
+
applied.push({ version: a.version, name: a.name });
|
|
116
|
+
}
|
|
117
|
+
results.push({ module: entry.module, ok: entry.ok, applied, error: entry.error });
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, output: { results, error: raw.error } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 目标产物最大迁移 seq(裁决:seq 从 applied[] 的 max(version) 取得);无任何 applied(no-op)返回 null */
|
|
123
|
+
export function maxAppliedSeq(output: MigrateOutput): number | null {
|
|
124
|
+
let max: number | null = null;
|
|
125
|
+
for (const r of output.results) {
|
|
126
|
+
for (const a of r.applied) {
|
|
127
|
+
if (max === null || a.version > max) {
|
|
128
|
+
max = a.version;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return max;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 禁降版守卫(纯函数,OJ-9 红线):true=放行——目标产物最大 seq ≥ 当前账本 head,或账本为空
|
|
137
|
+
* (0/null = 首装);目标 < head 返回 false(禁降版部署,M003 同源)。number/bigint 混比安全
|
|
138
|
+
* (关系运算符语义),调用方按各自信源的实际类型传入。
|
|
139
|
+
*/
|
|
140
|
+
export function rollbackGuard(targetMaxSeq: number | bigint, ledgerHead: number | bigint | null | undefined): boolean {
|
|
141
|
+
if (ledgerHead === null || ledgerHead === undefined || ledgerHead === 0 || ledgerHead === 0n) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return targetMaxSeq >= ledgerHead;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** R14 清理策略:完成后仅保留最近 3 份 .bak- */
|
|
148
|
+
const KEEP_BACKUPS = 3;
|
|
149
|
+
|
|
150
|
+
/** 字节块拼接(P2-13 流式路径:迁移输出恰一行 JSON 非行对齐,须攒全量再一次解码——块边界可切坏 UTF-8) */
|
|
151
|
+
function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
152
|
+
const out = new Uint8Array(chunks.reduce((n, c) => n + c.byteLength, 0));
|
|
153
|
+
let off = 0;
|
|
154
|
+
for (const c of chunks) {
|
|
155
|
+
out.set(c, off);
|
|
156
|
+
off += c.byteLength;
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
162
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @returns 备份路径 + 清理失败 note(null = 清理正常;note 并入迁移 message,禁 console——Finding A) */
|
|
166
|
+
async function backupDb(dbPath: string): Promise<{ bak: string, pruneNote: string | null }> {
|
|
167
|
+
const f = fsFace();
|
|
168
|
+
const bak = `${dbPath}.bak-${Date.now()}`;
|
|
169
|
+
await f.copyFile(dbPath, bak);
|
|
170
|
+
const pruneNote = await pruneBackups(dbPath, f);
|
|
171
|
+
return { bak, pruneNote };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 清理旧备份仅保留最近 KEEP_BACKUPS 份;失败不阻断迁移主流程(多留一份无害),note 上返进 message */
|
|
175
|
+
async function pruneBackups(dbPath: string, f: FsFace): Promise<string | null> {
|
|
176
|
+
const slash = dbPath.lastIndexOf("/");
|
|
177
|
+
const dir = slash === -1 ? "." : dbPath.slice(0, slash);
|
|
178
|
+
const base = slash === -1 ? dbPath : dbPath.slice(slash + 1);
|
|
179
|
+
const prefix = `${base}.bak-`;
|
|
180
|
+
let entries: string[];
|
|
181
|
+
try {
|
|
182
|
+
entries = await f.readdir(dir);
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
return `备份清理失败(.bak-* 全量保留,不影响迁移):${String(e)}`;
|
|
186
|
+
}
|
|
187
|
+
const stale = entries
|
|
188
|
+
.filter(e => e.startsWith(prefix))
|
|
189
|
+
.map((e) => {
|
|
190
|
+
const n = Number(e.slice(prefix.length));
|
|
191
|
+
return { e, ts: Number.isFinite(n) ? n : -1 };
|
|
192
|
+
})
|
|
193
|
+
.sort((a, b) => b.ts - a.ts)
|
|
194
|
+
.slice(KEEP_BACKUPS);
|
|
195
|
+
for (const { e } of stale) {
|
|
196
|
+
try {
|
|
197
|
+
await f.remove(`${dir}/${e}`, { recursive: true });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// 单份清理失败不掩盖迁移结果
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function migrate(ctx: MigrateContext): Promise<StageResult> {
|
|
207
|
+
const f = fsFace();
|
|
208
|
+
const modDir = `${ctx.staging}/api/${ctx.name}-${ctx.version}`;
|
|
209
|
+
try {
|
|
210
|
+
await f.readdir(modDir);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// 包内无 api 段(web-only 方案,§2.9):无迁移可跑,C2-31 复核已在 extracting 段钉过
|
|
214
|
+
// api 段声明与包内容的一致性——这里零 exec 零备份直接放行
|
|
215
|
+
return { stage: "migrating", ok: true, message: `迁移跳过:包内无 api/${ctx.name}-${ctx.version}/ 段(web-only 方案无迁移,C2 §2.9)` };
|
|
216
|
+
}
|
|
217
|
+
// 第一步 R14:前置备份——备份不可用即不迁移(fail-closed,exec 零调用,库保持原状)
|
|
218
|
+
let bak: string;
|
|
219
|
+
let pruneNote: string | null = null;
|
|
220
|
+
try {
|
|
221
|
+
({ bak, pruneNote } = await backupDb(ctx.dbPath));
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
return { stage: "migrating", ok: false, retryable: true, message: `迁移失败:前置备份不可用(${String(e)})——无备份不迁移(R14),未执行 oj migrate,库保持原状` };
|
|
225
|
+
}
|
|
226
|
+
// 第二步物化迁移服务目录(B-11):staging 兄弟目录、release 形态(扁平锁 + 模块目录拷贝);
|
|
227
|
+
// 锁恰一条目——oj 只 discover 本模块
|
|
228
|
+
const svcDir = migrateSvcDirOf(ctx.staging);
|
|
229
|
+
// 残留先清(上次崩溃遗留不污染本次输入);oj 真机实测(0.1.78):remove 不存在的路径即使
|
|
230
|
+
// force:true 也抛 TypeError: invalid_argument(deno ext 脚本对缺失目标的 force 语义)——
|
|
231
|
+
// 首装无残留是常态,此清除失败一律吞掉(真有阻碍物时下方 mkdir 自会报出真实错误)
|
|
232
|
+
try {
|
|
233
|
+
await f.remove(svcDir, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// 目标不存在 = 首装,正常路径(installer lockCommit 同一口径)
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await f.mkdir(svcDir, { recursive: true });
|
|
240
|
+
await f.writeFile(`${svcDir}/manifests.yaml`, new TextEncoder().encode(serializeLock({ [ctx.name]: ctx.version })));
|
|
241
|
+
await copyTree(modDir, `${svcDir}/${ctx.name}-${ctx.version}`, f);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
return { stage: "migrating", ok: false, retryable: true, message: `迁移失败:迁移服务目录物化失败(${String(e)})——附录 A migrating;备份 ${bak} 可供回退` };
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
// 第三步 exec:成败真源 = exit code(S1-6 实测;逐模块 continue-and-collect 由 oj 保证)。
|
|
248
|
+
// timeoutMs 显式取平台硬上限 600_000——oj 缺省 30_000 会杀生产级长迁移(0.1.78 文档化口径)。
|
|
249
|
+
// 双路径(P2-13,G2①):A exec.spawn 存在(feature-detect,oj 0.1.78+)→ for await 消费原始
|
|
250
|
+
// 字节,回调内自累积跨块拼接(迁移输出恰一行非行对齐)并逐块上报段内进度;stdout/stderr
|
|
251
|
+
// 都须消费(有界通道背压:不消费则子进程阻塞)。B 缺失 → exec.run 缓冲态(stage 粒度,
|
|
252
|
+
// 进度不增量刷)。流式形态超时/超限不抛错只 code=137(v0.1.79 订正)——与非零统一按失败收口。
|
|
253
|
+
const argv = buildMigrateCmd(svcDir, ctx.name);
|
|
254
|
+
let code: number;
|
|
255
|
+
let stdoutText: string;
|
|
256
|
+
let stderrTail = "";
|
|
257
|
+
// U5 取消安装(P3-8):进入 exec 前先探测——若已进入取消态,跳过 oj migrate(库保持原状,
|
|
258
|
+
// 等同 R14 fail-closed 的「不迁移」语义),直接返回 cancelled 终态由运行器回退
|
|
259
|
+
if (ctx.cancel?.()) {
|
|
260
|
+
return { stage: "migrating", ok: false, cancelled: true, retryable: true, message: "迁移被取消(用户中止,未执行 oj migrate,库保持原状)——回退到装前态(U5 取消安装)" };
|
|
261
|
+
}
|
|
262
|
+
const spawn = ctx.exec.spawn;
|
|
263
|
+
if (spawn !== undefined) {
|
|
264
|
+
const h = await spawn(argv, { timeoutMs: 600_000 });
|
|
265
|
+
const chunks: Uint8Array[] = [];
|
|
266
|
+
let total = 0;
|
|
267
|
+
let killed = false;
|
|
268
|
+
const dec = new TextDecoder();
|
|
269
|
+
const drainErr = (async () => {
|
|
270
|
+
for await (const c of h.stderr) {
|
|
271
|
+
stderrTail += dec.decode(c, { stream: true });
|
|
272
|
+
}
|
|
273
|
+
})();
|
|
274
|
+
try {
|
|
275
|
+
for await (const chunk of h.stdout) {
|
|
276
|
+
// U5 取消安装(P3-8):每收一块即探测 cancel token,命中 → h.kill() 提前终止流式消费
|
|
277
|
+
if (ctx.cancel?.()) {
|
|
278
|
+
killed = true;
|
|
279
|
+
h.kill();
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
chunks.push(chunk);
|
|
283
|
+
total += chunk.byteLength;
|
|
284
|
+
ctx.onProgress?.(total / (total + 4096));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// 子进程被 kill 后 stdout 通道可能抛错——killed 情形下转 cancel 终态,不误判失败
|
|
289
|
+
}
|
|
290
|
+
await drainErr;
|
|
291
|
+
code = (await h.exit()).code;
|
|
292
|
+
if (killed) {
|
|
293
|
+
return { stage: "migrating", ok: false, cancelled: true, retryable: true, message: `迁移被取消(用户中止,退出码 ${code})——staging 与已落位产物将回退到装前态(U5 取消安装)` };
|
|
294
|
+
}
|
|
295
|
+
stdoutText = dec.decode(concatBytes(chunks));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const r = await ctx.exec.run(argv, { timeoutMs: 600_000 });
|
|
299
|
+
code = r.code;
|
|
300
|
+
// U5 取消安装(P3-8):run 缓冲路径无法中途中断,返回前探测——已取消则按 cancelled 收口
|
|
301
|
+
if (ctx.cancel?.()) {
|
|
302
|
+
return { stage: "migrating", ok: false, cancelled: true, retryable: true, message: `迁移被取消(用户中止,退出码 ${code})——回退到装前态(U5 取消安装)` };
|
|
303
|
+
}
|
|
304
|
+
stdoutText = r.stdout;
|
|
305
|
+
stderrTail = r.stderr;
|
|
306
|
+
}
|
|
307
|
+
if (code !== 0) {
|
|
308
|
+
return {
|
|
309
|
+
stage: "migrating",
|
|
310
|
+
ok: false,
|
|
311
|
+
retryable: true,
|
|
312
|
+
message: `迁移失败:oj migrate exit ${code}(附录 A migrating:数据已保留原状,请修复后重试;备份 ${bak} 可供回退)——输出摘要:${(stderrTail || stdoutText).trim().slice(0, 500)}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
// 第四步解析:exit 0 但输出违约属契约破坏,重试不自愈(retryable=false)
|
|
316
|
+
const parsed = parseMigrateOutput(stdoutText);
|
|
317
|
+
if (!parsed.ok) {
|
|
318
|
+
return { stage: "migrating", ok: false, retryable: false, message: `迁移失败:${parsed.message}(备份 ${bak} 可供回退;附录 A migrating)` };
|
|
319
|
+
}
|
|
320
|
+
ctx.onMigrateOutput?.(parsed.output);
|
|
321
|
+
// 逐模块复核:exit 0 但含 ok=false 属输出自相矛盾,按失败处理(fail-closed)
|
|
322
|
+
const failed = parsed.output.results.filter(x => !x.ok);
|
|
323
|
+
if (failed.length > 0) {
|
|
324
|
+
return {
|
|
325
|
+
stage: "migrating",
|
|
326
|
+
ok: false,
|
|
327
|
+
retryable: true,
|
|
328
|
+
message: `迁移失败:${failed.length} 个模块迁移未通过(附录 A migrating:数据已保留原状;备份 ${bak} 可供回退)——${failed.map(x => `${x.module}: ${String(x.error ?? "ok=false")}`).join(";").slice(0, 500)}`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// 第五步禁降版红线(OJ-9,M003 同源):目标产物 max seq < 账本 head 拒切;applied 为空
|
|
332
|
+
// (no-op 同版重装)无从判定,交由 oj 自身 M003/missing-from-filesystem 门禁兜底
|
|
333
|
+
if (ctx.ledgerHead !== undefined) {
|
|
334
|
+
const target = maxAppliedSeq(parsed.output);
|
|
335
|
+
if (target !== null && !rollbackGuard(target, ctx.ledgerHead)) {
|
|
336
|
+
return {
|
|
337
|
+
stage: "migrating",
|
|
338
|
+
ok: false,
|
|
339
|
+
retryable: false,
|
|
340
|
+
message: `迁移失败:目标产物最大 seq ${target} 低于账本 head ${ctx.ledgerHead}——禁降版部署(OJ-9 红线,M003 同源);锁与状态保持原样(D11),备份 ${bak} 可供回退`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const appliedCount = parsed.output.results.reduce((n, x) => n + x.applied.length, 0);
|
|
345
|
+
return {
|
|
346
|
+
stage: "migrating",
|
|
347
|
+
ok: true,
|
|
348
|
+
message: `迁移完成:${parsed.output.results.length} 个模块、共 ${appliedCount} 条迁移已应用(账据 = oj migrate --format json 的 applied[];备份 ${bak},保留最近 ${KEEP_BACKUPS} 份${pruneNote ? `;${pruneNote}` : ""};迁移成功后才允许后续落锁,D11/R2)`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch (e) {
|
|
352
|
+
return { stage: "migrating", ok: false, retryable: true, message: `迁移失败:${String(e)}(备份 ${bak} 可供回退;附录 A migrating)` };
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
// svc 目录运行后清场(成败皆清):staging 本体原样保留(lock_commit 的 rename 源)
|
|
356
|
+
try {
|
|
357
|
+
await f.remove(svcDir, { recursive: true, force: true });
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// 清理失败不掩盖迁移结果(svc 目录不在锁内,release 装配忽略)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|