create-windy 0.6.6 → 0.6.7

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.
Files changed (36) hide show
  1. package/dist/cli.js +8 -0
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/Dockerfile +3 -2
  5. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +2 -1
  6. package/template/apps/web/Dockerfile +1 -0
  7. package/template/docs/operations/incremental-delivery.md +213 -0
  8. package/template/package.json +2 -1
  9. package/template/packages/delivery/bin/windy-delivery +2 -0
  10. package/template/packages/delivery/index.ts +14 -0
  11. package/template/packages/delivery/package.json +23 -0
  12. package/template/packages/delivery/src/archive-portability.test.ts +84 -0
  13. package/template/packages/delivery/src/archive.ts +247 -0
  14. package/template/packages/delivery/src/blob-store.ts +242 -0
  15. package/template/packages/delivery/src/builder.ts +199 -0
  16. package/template/packages/delivery/src/checksums.ts +83 -0
  17. package/template/packages/delivery/src/cli.ts +331 -0
  18. package/template/packages/delivery/src/delivery.test.ts +332 -0
  19. package/template/packages/delivery/src/digest.ts +31 -0
  20. package/template/packages/delivery/src/manifest.ts +255 -0
  21. package/template/packages/delivery/src/negotiation.ts +68 -0
  22. package/template/packages/delivery/src/pack.test.ts +165 -0
  23. package/template/packages/delivery/src/paths.ts +27 -0
  24. package/template/packages/delivery/src/process-command.ts +49 -0
  25. package/template/packages/delivery/src/redact.ts +14 -0
  26. package/template/packages/delivery/src/registry.test.ts +166 -0
  27. package/template/packages/delivery/src/registry.ts +148 -0
  28. package/template/packages/delivery/src/security.test.ts +41 -0
  29. package/template/packages/delivery/src/ssh-capabilities.ts +65 -0
  30. package/template/packages/delivery/src/ssh-process.ts +349 -0
  31. package/template/packages/delivery/src/ssh-sync.test.ts +236 -0
  32. package/template/packages/delivery/src/ssh-sync.ts +167 -0
  33. package/template/packages/delivery/src/staging.ts +241 -0
  34. package/template/packages/delivery/src/transfer.ts +54 -0
  35. package/template/packages/delivery/src/types.ts +78 -0
  36. package/template/packages/delivery/tsconfig.json +16 -0
package/dist/cli.js CHANGED
@@ -3282,6 +3282,7 @@ License \u7B7E\u53D1\u4E2D\u5FC3\u3001\u72EC\u7ACB\u7B7E\u540D\u670D\u52A1\u3001
3282
3282
  ${includeStorage ? "- \u5927\u62A5\u544A\u5BFC\u5165\uFF1A[\u53D7\u6CBB\u7406\u7684\u5927\u62A5\u544A\u6682\u5B58](./architecture/report-staging.md)\u3002" : ""}
3283
3283
  ${includeOfflineLicense ? "- License \u6D88\u8D39\u7AEF\uFF1A[\u79BB\u7EBF\u6FC0\u6D3B\u4E0E\u5B89\u88C5\u534F\u8BAE](./license/license-offline-activation-code.md)\uFF1B\u8FD0\u884C\u65F6\u80FD\u529B\u7531 npm \u4F9D\u8D56 `@southwind-ai/license` \u63D0\u4F9B\u3002" : ""}
3284
3284
  - \u8FD0\u7EF4\uFF1A[\u53EF\u89C2\u6D4B\u6027](./operations/observability.md)${includeOperations ? " \u4E0E [\u8FD0\u7EF4\u603B\u89C8](./platform/operations-overview.md)" : ""}\u3002
3285
+ - \u4EA4\u4ED8\uFF1A[\u6309 registry digest \u4E0E OCI blob \u5DEE\u91CF\u53D1\u5E03](./operations/incremental-delivery.md)\u3002
3285
3286
  ${includeAgent ? "- Agent\u3001\u591A\u6A21\u6001 Runtime \u4E0E MCP\uFF1A[Agent Runtime \u63A5\u5165](./platform/agent-runtime.md)\u3001[Document AI Runtime](./platform/document-ai-runtime.md)\u3001[Speech AI Runtime](./platform/speech-ai-runtime.md)\u3001[\u53D7\u63A7\u5DE5\u5177](./platform/agent-tools.md)\u4E0E[MCP \u4E1A\u52A1\u80FD\u529B](./architecture/mcp-business-capabilities.md)\u3002" : ""}
3286
3287
  - \u517C\u5BB9\uFF1A[RuoYi-Vue Adapter](./adapters/ruoyi-vue-compatibility.md)\u3002
3287
3288
  `;
@@ -3659,6 +3660,7 @@ var templateDirectories = [
3659
3660
  "packages/config",
3660
3661
  "packages/crud-generator",
3661
3662
  "packages/database",
3663
+ "packages/delivery",
3662
3664
  "packages/example-work-order",
3663
3665
  "packages/jobs",
3664
3666
  "packages/modules",
@@ -3694,6 +3696,7 @@ var templateDocumentationFiles = [
3694
3696
  "docs/development/module-lifecycle.md",
3695
3697
  "docs/license/license-offline-activation-code.md",
3696
3698
  "docs/operations/observability.md",
3699
+ "docs/operations/incremental-delivery.md",
3697
3700
  "docs/platform/agent-run-attestation.md",
3698
3701
  "docs/platform/agent-tools.md",
3699
3702
  "docs/platform/agent-runtime.md",
@@ -7784,6 +7787,11 @@ var recipes = [
7784
7787
  id: "starter-0.6.5-to-0.6.6",
7785
7788
  from: "0.6.5",
7786
7789
  to: "0.6.6"
7790
+ },
7791
+ {
7792
+ id: "starter-0.6.6-to-0.6.7",
7793
+ from: "0.6.6",
7794
+ to: "0.6.7"
7787
7795
  }
7788
7796
  ];
7789
7797
  function resolveRecipeChain(sourceVersion, targetVersion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "aebc81cd3a97d7bc3f13c600afb7dc13bbf9c3d0",
4
- "generatorVersion": "0.6.6",
3
+ "templateCommit": "dcc27b44de561eb3b4e6f9aea7869ba0f4299a62",
4
+ "generatorVersion": "0.6.7",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -15,6 +15,7 @@ COPY packages/config/package.json packages/config/package.json
15
15
  COPY packages/create-windy/package.json packages/create-windy/package.json
16
16
  COPY packages/crud-generator/package.json packages/crud-generator/package.json
17
17
  COPY packages/database/package.json packages/database/package.json
18
+ COPY packages/delivery/package.json packages/delivery/package.json
18
19
  COPY packages/example-work-order/package.json packages/example-work-order/package.json
19
20
  COPY packages/jobs/package.json packages/jobs/package.json
20
21
  COPY packages/license-sdk/package.json packages/license-sdk/package.json
@@ -35,8 +36,8 @@ RUN set -eu; \
35
36
  echo "$WINDY_DEBIAN_MIRROR" | grep -Eq '^https?://[A-Za-z0-9./:_-]+$'; \
36
37
  echo "$WINDY_DEBIAN_SECURITY_MIRROR" | grep -Eq '^https?://[A-Za-z0-9./:_-]+$'; \
37
38
  sed -i "s|http://deb.debian.org/debian-security|$WINDY_DEBIAN_SECURITY_MIRROR|g; s|http://deb.debian.org/debian|$WINDY_DEBIAN_MIRROR|g" /etc/apt/sources.list.d/debian.sources; \
38
- if ! timeout --kill-after=5s 90s sh -c 'apt-get update -o Acquire::http::AllowRedirect=false -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Retries=0 && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --yes build-essential libarchive-dev libarchive-tools'; then \
39
- echo "APT 包源不可用或 90 秒总时限已到:无法安装 report-staging 依赖" >&2; exit 70; \
39
+ if ! timeout --kill-after=5s 180s sh -c 'apt-get update -o Acquire::http::AllowRedirect=false -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Retries=0 && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --yes build-essential libarchive-dev libarchive-tools'; then \
40
+ echo "APT 包源不可用或 180 秒总时限已到:无法安装 report-staging 依赖" >&2; exit 70; \
40
41
  fi
41
42
  # @windy-module system.storage end
42
43
  COPY drizzle.config.ts drizzle.config.ts
@@ -17,8 +17,9 @@ describe("AI Runtime Actor handle", () => {
17
17
  test("篡改或更换密钥后 fail closed", () => {
18
18
  const codec = new AiRuntimeActorHandleCodec("internal-token-for-tests");
19
19
  const handle = codec.issue("user-1");
20
+ const tampered = `${handle.slice(0, -1)}${handle.endsWith("A") ? "B" : "A"}`;
20
21
 
21
- expect(codec.verify(`${handle.slice(0, -1)}A`)).toBeUndefined();
22
+ expect(codec.verify(tampered)).toBeUndefined();
22
23
  expect(
23
24
  new AiRuntimeActorHandleCodec("different-internal-token").verify(handle),
24
25
  ).toBeUndefined();
@@ -15,6 +15,7 @@ COPY packages/config/package.json packages/config/package.json
15
15
  COPY packages/create-windy/package.json packages/create-windy/package.json
16
16
  COPY packages/crud-generator/package.json packages/crud-generator/package.json
17
17
  COPY packages/database/package.json packages/database/package.json
18
+ COPY packages/delivery/package.json packages/delivery/package.json
18
19
  COPY packages/example-work-order/package.json packages/example-work-order/package.json
19
20
  COPY packages/jobs/package.json packages/jobs/package.json
20
21
  COPY packages/license-sdk/package.json packages/license-sdk/package.json
@@ -0,0 +1,213 @@
1
+ # 增量交付与确定性归档
2
+
3
+ Windy 的交付传输层提供两条增量路径,并保留完整 bundle 作为兼容和真正离线
4
+ fallback。传输层只改变“镜像字节如何到达目标机”,不会替代现有部署事务中的数据库
5
+ 备份、volume 冻结、migration、readiness、安装账本原子提交和失败回滚。
6
+
7
+ ## 不变量
8
+
9
+ - release manifest 绑定完整 Git SHA、目标平台、镜像角色、镜像 config digest、registry
10
+ digest、Docker archive 文件图和所有内容 blob。
11
+ - 在线镜像事实必须是 `repository@sha256:<64 hex>`;tag 仅可作为发布入口,不能写入
12
+ 安装账本或作为已部署版本证明。
13
+ - V2 manifest 自身在传输前计算 SHA-256,目标机在 stage 前重新计算。每个 blob 同时
14
+ 校验声明尺寸和 SHA-256;缺失、损坏、重复路径、路径穿越或 schema 不支持都会停止。
15
+ - blob 传输完成不等于部署完成。只有旧部署事务完成备份、镜像 load/pull、migration、
16
+ readiness 和账本提交后,release 才生效。
17
+ - 失败的传输保留已校验完整 blob 和合法 partial,以便续传;失败 stage 会删除自己的
18
+ nonce 目录,不会删除当前 release、备份或其它 owner 的资源。
19
+
20
+ ## Manifest V2
21
+
22
+ `DeliveryManifestV2` 使用 `schemaVersion=2` 和 `minimumReaderVersion=2`。核心字段为:
23
+
24
+ - `images[]`:稳定 role、platform、Docker config digest,以及可选的 registry repository
25
+ 与 manifest digest、layer digest/size;repository 不允许包含 tag、digest、scheme 或
26
+ 凭据。config、manifest 和 layer descriptor 必须绑定归档中的同一内容 blob。
27
+ - `dockerArchive`:格式固定为 `oci-image-layout-v1`;`entries[]` 绑定重建离线 OCI image
28
+ layout 所需的相对路径、内容 digest、size 和 mode。
29
+ - `releaseFiles[]`:除 `images.tar` 外的完整 release 文件图;stage 会连同重建的
30
+ `images.tar` 一起原子落盘,不只生成镜像归档。
31
+ - `blobs[]`:去重后的内容寻址集合;多个 archive entry 可复用同一个 blob。
32
+ - `compatibility.fullBundle`:可选 V1 完整 bundle 的文件名、digest 和 size。
33
+
34
+ reader 必须先协商能力:
35
+
36
+ 1. 目标支持 registry digest 且 manifest 中每个镜像都有 registry 事实时,优先在线拉取。
37
+ 2. 否则,双方支持 V2、blob delta 和 resumable transfer 时,使用 SSH blob 差量。
38
+ 3. 否则,只有 manifest 显式携带 V1 fallback 时才回退完整 bundle。
39
+ 4. 没有共同 schema 时直接失败;不得把未知 V2 当成 V1,也不得静默上传完整包。
40
+
41
+ ## 在线 registry 路径
42
+
43
+ 发布端把镜像推送到标准 OCI Registry(腾讯云 TCR 等),冻结 registry 返回的 manifest
44
+ digest。目标机按 digest pull:
45
+
46
+ ```text
47
+ ccr.ccs.tencentyun.com/example/windy-server@sha256:...
48
+ ```
49
+
50
+ Docker/Containerd 会根据本地 content store 只下载缺失 layer。pull 后必须再次 inspect,
51
+ 确认目标 `repo@digest` 的 image config digest 与 manifest 一致,再把 digest-pinned image
52
+ 变量交给现有部署事务。Registry 登录由 Docker credential helper 或目标机受控 Secret
53
+ 管理;manifest、日志和统计不得包含用户名、密码、token、签名 URL 或 Docker config。
54
+
55
+ 在线统计以 OCI descriptor 为单位记录总 bytes/blob、已存在 bytes/blob 和 Docker 报告
56
+ `Pull complete` 的 descriptor bytes/blob。当前 `pull-registry` 无法从 Docker CLI 审计
57
+ 部分缓存、续传及 HTTP/manifest/config 协议开销,因此 `uploadedBytes` 与
58
+ `uploadedBlobs` 明确返回 `null`,另以 `descriptorPulledBytes` /
59
+ `descriptorPulledBlobs` 报告 descriptor 状态;不得把它冒充精确网络字节。无法取得逐
60
+ layer 状态时直接失败,也不用镜像虚拟大小估算。
61
+
62
+ 目标机执行示例:
63
+
64
+ ```bash
65
+ manifest_digest="$(tr -d '\n' < ./DELIVERY-MANIFEST.sha256)"
66
+ bun run delivery:remote -- pull-registry \
67
+ --manifest-digest "$manifest_digest" \
68
+ < ./delivery-manifest.v2.json
69
+ ```
70
+
71
+ 命令只接受 manifest 中的 `repository@digest`,pull 后用 Docker image config digest 二次
72
+ 验真,再输出 descriptor 总量、复用量与上述可审计性标记。镜像推送与
73
+ digest 冻结由发布流水线的 OCI Registry adapter 完成;adapter 必须把 Registry 返回的
74
+ manifest digest 和 descriptor size 写入 `images.json`,不能把输入 tag 当作冻结事实。
75
+
76
+ ## SSH / 离线 blob 差量
77
+
78
+ 生成物中的 `@southwind-ai/delivery` 是 Starter 私有 workspace package。远端命令入口:
79
+
80
+ ```bash
81
+ bun run delivery:remote -- capabilities
82
+ ```
83
+
84
+ 协调器的协议顺序固定为:
85
+
86
+ 1. 调用 `capabilities`,协商 schema 和传输方式。
87
+ 2. 在安装根目录用原子 `mkdir` 获取 `.delivery-lock`,写入随机 owner token;并发部署失败。
88
+ 3. 把 manifest 的 blob 描述发送给 `inventory`。远端逐个重新计算尺寸和 SHA-256,损坏
89
+ blob 不计为复用。
90
+ 4. 对缺失 blob 调用 `partial` 获取已落盘 offset。发送端只从该 offset 继续;接收端要求
91
+ offset 精确相等、每段 `fsync`,完整后校验 digest 并原子 rename。
92
+ 5. `stage` 重新验证 manifest 和所有 blob,检查空间预算,在随机 nonce 目录重建确定性
93
+ OCI `images.tar` 与其余 release 文件,并验证 `CHECKSUMS.sha256` 的精确集合和每个
94
+ digest,再把包含 `release/` 与 delivery sidecar 的目录原子 rename 到
95
+ `releases/<releaseId>`。既有部署事务只消费其中的 `release/`,delivery sidecar 不污染
96
+ 原 verifier 的文件集合。
97
+ 6. 释放 owner 匹配的锁。后续由项目既有 `update-offline` 或生产发布事务消费 stage 目录。
98
+
99
+ 远端低层命令使用 stdin 传 manifest/blob 列表和 blob bytes,不把内容放入 shell 参数。
100
+ 本地 `SshProcessTransport` 用参数数组调用 `ssh`,目标、二进制和远端路径均先做字符与
101
+ 路径边界校验。错误输出会截断并脱敏 credential marker。
102
+
103
+ `packages/delivery/bin/windy-delivery` 是受管的远端 launcher;它不注册为 workspace
104
+ package `bin`,避免 fresh `bun install` 改写受管源码的 POSIX mode。第一次启用 V2 前,
105
+ 从已验真的 Starter/release SHA 把整个 `packages/delivery` 安装到版本目录,再原子切换
106
+ 稳定入口(以下命令在目标 Linux 上执行):
107
+
108
+ ```bash
109
+ sudo install -d -m 0755 /opt/windy/runtime /opt/windy/bin
110
+ sudo cp -R ./packages/delivery /opt/windy/runtime/delivery-0.6.7.tmp
111
+ sudo chmod 0555 /opt/windy/runtime/delivery-0.6.7.tmp/bin/windy-delivery
112
+ sudo mv /opt/windy/runtime/delivery-0.6.7.tmp \
113
+ /opt/windy/runtime/delivery-0.6.7
114
+ sudo ln -s /opt/windy/runtime/delivery-0.6.7/bin/windy-delivery \
115
+ /opt/windy/bin/windy-delivery.next
116
+ sudo mv -Tf /opt/windy/bin/windy-delivery.next \
117
+ /opt/windy/bin/windy-delivery
118
+ /opt/windy/bin/windy-delivery capabilities
119
+ ```
120
+
121
+ 版本目录必须来自已经校验的 release,不从 registry tag、临时工作树或目标机网络重新
122
+ 生成。若目标还没有 launcher,能力探测只允许按下文显式回退 V1 完整 bundle;SSH
123
+ 鉴权、权限、摘要或原子切换失败都不得降级。保留上一版本目录,直到新 launcher 完成
124
+ `capabilities` 与一次部署事务;回滚只需把稳定 symlink 原子切回上一版本。
125
+
126
+ 空间预算在上传缺失 blob 前执行,分别检查 blob store 与 install root 所在文件系统;同一
127
+ 挂载点按缺失 blob、完整 release、delivery sidecar、重建 OCI archive 的
128
+ header/padding/payload 和 512 MiB 默认安全余量合并预算,不同挂载点分别 fail closed。
129
+ blob store 的保留/回收策略必须以已提交 release、
130
+ previous release 和未过期 partial 的引用闭包为依据;不得用目录 mtime 猜测后直接删除。
131
+
132
+ V2 差量示例(`auto` 在具备 registry facts 时只返回 `registry-preferred`,不会暗中改走
133
+ SSH;需要 SSH 时显式选择 `blob-delta`):
134
+
135
+ ```bash
136
+ manifest_digest="$(tr -d '\n' < ./DELIVERY-MANIFEST.sha256)"
137
+ bun run delivery:remote -- sync-ssh \
138
+ --manifest ./delivery-manifest.v2.json \
139
+ --manifest-digest "$manifest_digest" \
140
+ --store . \
141
+ --target deploy@example.internal \
142
+ --remote-binary /opt/windy/bin/windy-delivery \
143
+ --remote-store /opt/windy/blob-store \
144
+ --remote-install-root /opt/windy \
145
+ --remote-fallback-root /opt/windy/fallback \
146
+ --mode blob-delta
147
+ ```
148
+
149
+ 旧端或显式兼容模式使用同一命令的 `--mode full-bundle --fallback
150
+ ./full-bundle.tar.gz`。fallback 同样持有 owner 锁并预检目标挂载点;完整包先传到
151
+ owner-scoped partial,摘要核对成功后 no-clobber 原子 rename,失败只清理自己的 partial。
152
+ 远端已有同 digest 文件会复用,同名不同 digest 会 fail closed。capability 只有明确的旧端
153
+ “命令不存在/不支持”响应才可降级;SSH 鉴权、网络、权限或畸形 JSON 一律中止。
154
+
155
+ ## 传输统计
156
+
157
+ 每次 blob 计划和实际传输输出以下不含凭据的整数:
158
+
159
+ - `totalBytes` / `totalBlobs`:当前 release 的去重 blob 集合。
160
+ - `reusedBytes` / `reusedBlobs`:远端完整 blob,加上本次从 partial 续传前已存在的 bytes。
161
+ - `uploadedBytes` / `uploadedBlobs`:本次真正写入 SSH 的 bytes 和涉及的 blob 数。
162
+
163
+ 统计来自内容 descriptor 和接收端确认,不解析 registry URL,也不输出命令环境变量。
164
+
165
+ ## 确定性归档与 macOS
166
+
167
+ 正式 `.tar.gz` 和重建的 `images.tar` 由 Windy 的 ustar writer 按排序后的显式文件集合
168
+ 生成:uid/gid/mtime 固定为 0,owner/group 固定为 root,mode 归一化,不写 pax xattr、
169
+ resource fork 或 Finder metadata。路径组件以 `._` 开头时直接拒绝。因此 macOS 上即使
170
+ 源目录具有 extended attributes,归档也不会产生 AppleDouble `._*`。
171
+
172
+ 不要改回普通 `tar -czf` 依赖宿主机默认行为。`COPYFILE_DISABLE=1` 只作为调用系统 tar
173
+ 读取/验收时的额外保护,不是正式修复本身。回归测试在 macOS 写入真实 xattr,再把归档
174
+ 交给无网络的 Alpine 容器解压并比较精确文件集合。
175
+
176
+ ## 构建与旧项目升级
177
+
178
+ `create-windy@0.6.7` 候选把 `packages/delivery`、本说明和 `delivery:remote` 脚本加入
179
+ 受管 Starter。`0.6.6 -> 0.6.7` 通过连续 recipe 事务升级;现有 project-owned 发布脚本
180
+ 不会被覆盖。正式构建先准备一个完整 release 目录,其中 `images.tar` 必须是 OCI image
181
+ layout(含 `oci-layout`、`index.json` 和 `blobs/sha256/*`),再运行:
182
+
183
+ ```bash
184
+ bun run delivery:remote -- pack \
185
+ --release-dir ./.artifacts/release \
186
+ --output-dir ./.artifacts/delivery \
187
+ --product windy \
188
+ --release-id 2026.08.08-1 \
189
+ --git-sha "$release_git_sha" \
190
+ --platform linux/amd64 \
191
+ --created-at 2026-08-08T00:00:00.000Z \
192
+ --images-json ./.artifacts/images.json
193
+ ```
194
+
195
+ `pack` 不改输入目录:它在临时目录重建确定性的 OCI `images.tar`,重新生成覆盖最终精确
196
+ 文件集合的 `CHECKSUMS.sha256`,并以这组相同字节同时构建 V1 与 V2。输出目录独立包含
197
+ `full-bundle.tar.gz`、`delivery-manifest.v2.json`、
198
+ `DELIVERY-MANIFEST.sha256` 和 `blobs/sha256/*`。`images.json` 由 Registry/OCI exporter
199
+ 生成,必须记录每个镜像的 role、platform、config digest,以及可选的 repository、
200
+ manifest digest 和 layer digest/size。`pack` 会拒绝普通 Docker save archive、缺失 OCI
201
+ layout、digest 路径不一致、镜像 descriptor 未绑定归档 blob 或输出目录覆盖。
202
+
203
+ 旧项目按以下顺序接入:
204
+
205
+ 1. 先让现有 build-release 生成 OCI layout,并调用 `pack` 同时产出独立 V1 完整 bundle
206
+ 与 V2 manifest/blob store;不要用 rsync 整个 `images.tar` 伪装差量。
207
+ 2. 在线发布增加 registry push 与 digest 冻结,目标事务改用 digest-pinned refs。
208
+ 3. SSH wrapper 接入 `SshProcessTransport`,stage 成功后仍调用原 `update-offline`。
209
+ 4. 至少连续保留一个兼容周期的 V1 bundle;确认所有安装端支持 V2 后,才可由产品版本
210
+ 决策是否停止生成 fallback。
211
+
212
+ 升级器的三方合并、备份和冲突处理规则不变。客户已有同名 project-owned 文件时必须
213
+ 报告真实冲突,不能用 `--force` 或手工复制覆盖。
@@ -52,6 +52,7 @@
52
52
  "test:ops:tool-sandbox": "bun run scripts/tool-sandbox-isolation-regression.ts",
53
53
  "ops:db": "bun run scripts/production-database.ts",
54
54
  "ops:release": "bun run scripts/production-release.ts",
55
+ "delivery:remote": "bun run packages/delivery/src/cli.ts",
55
56
  "test:merge": "bun run scripts/merge-gate.ts",
56
57
  "release:verify": "bun run scripts/release-verify.ts --full",
57
58
  "release:candidate": "bun run scripts/release-candidate.ts",
@@ -65,7 +66,7 @@
65
66
  "check:document": "uv run --directory apps/document-server ruff check . && uv run --directory apps/document-server ruff format --check . && uv run --directory apps/document-server mypy",
66
67
  "check:speech": "uv run --directory apps/speech-server ruff check . && uv run --directory apps/speech-server ruff format --check . && uv run --directory apps/speech-server mypy",
67
68
  "check:embedding": "uv run --directory apps/embedding-server ruff check . && uv run --directory apps/embedding-server ruff format --check . && uv run --directory apps/embedding-server mypy",
68
- "typecheck": "bun run --cwd apps/web typecheck && bun run --cwd apps/server typecheck && bun run --cwd apps/license typecheck && bun run --cwd apps/signing-service typecheck && bun run --cwd packages/ai-client typecheck && bun run --cwd packages/license-sdk typecheck && bun run --cwd packages/jobs typecheck && bun run --cwd packages/server-sdk typecheck && bun run --cwd packages/storage typecheck && bun run --cwd packages/database typecheck && bun run --cwd packages/crud-generator typecheck && bun run --cwd packages/create-windy typecheck",
69
+ "typecheck": "bun run --cwd apps/web typecheck && bun run --cwd apps/server typecheck && bun run --cwd apps/license typecheck && bun run --cwd apps/signing-service typecheck && bun run --cwd packages/ai-client typecheck && bun run --cwd packages/license-sdk typecheck && bun run --cwd packages/jobs typecheck && bun run --cwd packages/server-sdk typecheck && bun run --cwd packages/storage typecheck && bun run --cwd packages/database typecheck && bun run --cwd packages/crud-generator typecheck && bun run --cwd packages/delivery typecheck && bun run --cwd packages/create-windy typecheck",
69
70
  "lint": "oxlint .",
70
71
  "format": "oxfmt",
71
72
  "check:static": "bun run lint && bun run typecheck && bun run check:agent && bun run check:document && bun run check:speech && bun run check:embedding",
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ await import("../src/cli.ts");
@@ -0,0 +1,14 @@
1
+ export * from "./src/archive.js";
2
+ export * from "./src/blob-store.js";
3
+ export * from "./src/builder.js";
4
+ export * from "./src/checksums.js";
5
+ export * from "./src/digest.js";
6
+ export * from "./src/manifest.js";
7
+ export * from "./src/negotiation.js";
8
+ export * from "./src/registry.js";
9
+ export * from "./src/redact.js";
10
+ export * from "./src/staging.js";
11
+ export * from "./src/ssh-process.js";
12
+ export * from "./src/ssh-sync.js";
13
+ export * from "./src/transfer.js";
14
+ export * from "./src/types.js";
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@southwind-ai/delivery",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "engines": {
8
+ "bun": ">=1.3.14"
9
+ },
10
+ "files": [
11
+ "bin/windy-delivery",
12
+ "index.ts",
13
+ "src/**/*.ts",
14
+ "!src/**/*.test.ts"
15
+ ],
16
+ "scripts": {
17
+ "delivery": "bun run src/cli.ts",
18
+ "typecheck": "tsc --noEmit --project tsconfig.json"
19
+ },
20
+ "exports": {
21
+ ".": "./index.ts"
22
+ }
23
+ }
@@ -0,0 +1,84 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createDeterministicTarGz, listTarGzEntries } from "./archive.js";
6
+ import { listArchiveFiles } from "./builder.js";
7
+
8
+ const roots: string[] = [];
9
+ const acceptanceTest =
10
+ process.env.WINDY_DELIVERY_LINUX_ACCEPTANCE === "1" &&
11
+ process.platform === "darwin"
12
+ ? test
13
+ : test.skip;
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(
17
+ roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
18
+ );
19
+ });
20
+
21
+ acceptanceTest(
22
+ "macOS xattr 输入经确定性构建后在 Linux 解压保持精确文件集合",
23
+ async () => {
24
+ const root = await mkdtemp(join(tmpdir(), "windy-archive-portability-"));
25
+ roots.push(root);
26
+ const input = join(root, "input");
27
+ const output = join(root, "linux");
28
+ const archive = join(root, "bundle.tar.gz");
29
+ await mkdir(join(input, "bin"), { recursive: true });
30
+ await writeFile(join(input, "manifest.json"), "{}\n");
31
+ await writeFile(join(input, "bin/delivery"), "#!/bin/sh\n", {
32
+ mode: 0o755,
33
+ });
34
+ await run([
35
+ "xattr",
36
+ "-w",
37
+ "com.southwind.delivery",
38
+ "portable-metadata",
39
+ join(input, "manifest.json"),
40
+ ]);
41
+ await createDeterministicTarGz(archive, [
42
+ { source: join(input, "manifest.json"), path: "manifest.json" },
43
+ {
44
+ source: join(input, "bin/delivery"),
45
+ path: "bin/delivery",
46
+ mode: 0o755,
47
+ },
48
+ ]);
49
+ expect(await listTarGzEntries(archive)).toEqual([
50
+ "bin/delivery",
51
+ "manifest.json",
52
+ ]);
53
+ await mkdir(output);
54
+ await run([
55
+ "docker",
56
+ "run",
57
+ "--rm",
58
+ "--network",
59
+ "none",
60
+ "-v",
61
+ `${root}:/work`,
62
+ "alpine:3.21",
63
+ "sh",
64
+ "-eu",
65
+ "-c",
66
+ "tar -xzf /work/bundle.tar.gz -C /work/linux",
67
+ ]);
68
+ expect(await listArchiveFiles(output)).toEqual([
69
+ "bin/delivery",
70
+ "manifest.json",
71
+ ]);
72
+ expect(await readFile(join(output, "manifest.json"), "utf8")).toBe("{}\n");
73
+ },
74
+ 30_000,
75
+ );
76
+
77
+ async function run(command: string[]): Promise<void> {
78
+ const child = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
79
+ const [exitCode, stderr] = await Promise.all([
80
+ child.exited,
81
+ new Response(child.stderr).text(),
82
+ ]);
83
+ if (exitCode !== 0) throw new Error(`${command[0]} 失败:${stderr.trim()}`);
84
+ }