create-windy 0.6.6 → 0.6.8

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 (51) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +11 -68
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/apps/agent-server/README.md +4 -4
  6. package/template/apps/server/Dockerfile +3 -2
  7. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +2 -1
  8. package/template/apps/server/src/data-access/drizzle-predicate.ts +16 -6
  9. package/template/apps/web/Dockerfile +1 -0
  10. package/template/docs/architecture/ai-egress.md +1 -1
  11. package/template/docs/architecture/ai-runtime-readiness.md +2 -2
  12. package/template/docs/architecture/ai-runtime.md +15 -15
  13. package/template/docs/architecture/module-secret-resolution.md +4 -4
  14. package/template/docs/architecture/report-staging.md +1 -1
  15. package/template/docs/development/custom-login-page.md +3 -3
  16. package/template/docs/license/license-offline-activation-code.md +2 -2
  17. package/template/docs/operations/incremental-delivery.md +213 -0
  18. package/template/docs/platform/agent-runtime.md +4 -4
  19. package/template/docs/platform/document-ai-runtime.md +3 -3
  20. package/template/docs/platform/speech-ai-runtime.md +1 -1
  21. package/template/docs/platform/vision-ai-runtime.md +1 -1
  22. package/template/docs/product/built-in-roles.md +9 -1
  23. package/template/package.json +2 -1
  24. package/template/packages/delivery/bin/windy-delivery +2 -0
  25. package/template/packages/delivery/index.ts +14 -0
  26. package/template/packages/delivery/package.json +23 -0
  27. package/template/packages/delivery/src/archive-portability.test.ts +84 -0
  28. package/template/packages/delivery/src/archive.ts +247 -0
  29. package/template/packages/delivery/src/blob-store.ts +242 -0
  30. package/template/packages/delivery/src/builder.ts +199 -0
  31. package/template/packages/delivery/src/checksums.ts +83 -0
  32. package/template/packages/delivery/src/cli.ts +331 -0
  33. package/template/packages/delivery/src/delivery.test.ts +332 -0
  34. package/template/packages/delivery/src/digest.ts +31 -0
  35. package/template/packages/delivery/src/manifest.ts +255 -0
  36. package/template/packages/delivery/src/negotiation.ts +68 -0
  37. package/template/packages/delivery/src/pack.test.ts +165 -0
  38. package/template/packages/delivery/src/paths.ts +27 -0
  39. package/template/packages/delivery/src/process-command.ts +49 -0
  40. package/template/packages/delivery/src/redact.ts +14 -0
  41. package/template/packages/delivery/src/registry.test.ts +166 -0
  42. package/template/packages/delivery/src/registry.ts +148 -0
  43. package/template/packages/delivery/src/security.test.ts +41 -0
  44. package/template/packages/delivery/src/ssh-capabilities.ts +65 -0
  45. package/template/packages/delivery/src/ssh-process.ts +349 -0
  46. package/template/packages/delivery/src/ssh-sync.test.ts +236 -0
  47. package/template/packages/delivery/src/ssh-sync.ts +167 -0
  48. package/template/packages/delivery/src/staging.ts +241 -0
  49. package/template/packages/delivery/src/transfer.ts +54 -0
  50. package/template/packages/delivery/src/types.ts +78 -0
  51. package/template/packages/delivery/tsconfig.json +16 -0
@@ -0,0 +1,83 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { sha256File } from "./digest.js";
4
+ import { assertRelativePortablePath } from "./paths.js";
5
+
6
+ export const releaseChecksumFile = "CHECKSUMS.sha256";
7
+
8
+ export async function writeReleaseChecksums(
9
+ root: string,
10
+ paths: readonly string[],
11
+ ): Promise<void> {
12
+ const normalized = normalizePaths(paths);
13
+ const lines: string[] = [];
14
+ for (const path of normalized) {
15
+ const digest = await sha256File(join(resolve(root), path));
16
+ lines.push(`${digest.slice("sha256:".length)} ${path}`);
17
+ }
18
+ await writeFile(
19
+ join(resolve(root), releaseChecksumFile),
20
+ `${lines.join("\n")}\n`,
21
+ {
22
+ flag: "wx",
23
+ mode: 0o644,
24
+ },
25
+ );
26
+ }
27
+
28
+ export async function verifyReleaseChecksums(
29
+ root: string,
30
+ expectedPaths: readonly string[],
31
+ ): Promise<void> {
32
+ const expected = normalizePaths(
33
+ expectedPaths.filter((path) => path !== releaseChecksumFile),
34
+ );
35
+ const records = parseChecksums(
36
+ await readFile(join(resolve(root), releaseChecksumFile), "utf8"),
37
+ );
38
+ if (
39
+ records.length !== expected.length ||
40
+ records.some((record, index) => record.path !== expected[index])
41
+ ) {
42
+ throw new Error("release CHECKSUMS 文件集合不匹配");
43
+ }
44
+ for (const record of records) {
45
+ if (
46
+ (await sha256File(join(resolve(root), record.path))) !== record.digest
47
+ ) {
48
+ throw new Error(`release CHECKSUMS digest 不匹配:${record.path}`);
49
+ }
50
+ }
51
+ }
52
+
53
+ function parseChecksums(
54
+ value: string,
55
+ ): { path: string; digest: `sha256:${string}` }[] {
56
+ const records = value
57
+ .split(/\r?\n/)
58
+ .filter(Boolean)
59
+ .map((line) => {
60
+ const match = /^([a-f0-9]{64}) {2}(.+)$/.exec(line);
61
+ if (!match) throw new Error("release CHECKSUMS 格式无效");
62
+ return {
63
+ digest: `sha256:${match[1]}` as const,
64
+ path: assertRelativePortablePath(match[2]!),
65
+ };
66
+ })
67
+ .sort((left, right) => left.path.localeCompare(right.path));
68
+ if (new Set(records.map(({ path }) => path)).size !== records.length) {
69
+ throw new Error("release CHECKSUMS 路径重复");
70
+ }
71
+ return records;
72
+ }
73
+
74
+ function normalizePaths(paths: readonly string[]): string[] {
75
+ const normalized = paths
76
+ .map(assertRelativePortablePath)
77
+ .filter((path) => path !== releaseChecksumFile)
78
+ .sort();
79
+ if (new Set(normalized).size !== normalized.length) {
80
+ throw new Error("release 文件路径重复");
81
+ }
82
+ return normalized;
83
+ }
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env bun
2
+ import { randomBytes } from "node:crypto";
3
+ import {
4
+ mkdir,
5
+ mkdtemp,
6
+ readFile,
7
+ rename,
8
+ rm,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { extractPortableTar } from "./archive.js";
13
+ import {
14
+ inventoryBlobs,
15
+ partialBlobSize,
16
+ receiveBlobStream,
17
+ } from "./blob-store.js";
18
+ import {
19
+ buildDeliveryManifest,
20
+ createFullBundleFallback,
21
+ prepareDeterministicRelease,
22
+ } from "./builder.js";
23
+ import { assertSha256Digest } from "./digest.js";
24
+ import {
25
+ manifestDigest,
26
+ readManifest,
27
+ validateManifest,
28
+ writeManifest,
29
+ } from "./manifest.js";
30
+ import { SshProcessTransport } from "./ssh-process.js";
31
+ import { syncReleaseOverSsh } from "./ssh-sync.js";
32
+ import { pullRegistryRelease } from "./registry.js";
33
+ import {
34
+ acquireDeliveryLock,
35
+ preflightReleaseSpace,
36
+ releaseDeliveryLock,
37
+ stageRelease,
38
+ type DeliveryLock,
39
+ verifyDeliveryLock,
40
+ } from "./staging.js";
41
+ import type { ContentBlob, ReleaseImageV2, ReleasePlatform } from "./types.js";
42
+
43
+ const capabilities = {
44
+ schemaVersions: [1, 2],
45
+ registryByDigest: true,
46
+ blobDelta: true,
47
+ resumableBlobTransfer: true,
48
+ } as const;
49
+
50
+ const [command, ...argv] = process.argv.slice(2);
51
+
52
+ if (command === "pack") {
53
+ const options = parseOptions(argv);
54
+ const releaseRoot = requiredPath(options, "release-dir");
55
+ const outputDirectory = requiredPath(options, "output-dir");
56
+ await mkdir(dirname(outputDirectory), { recursive: true });
57
+ const staging = await mkdtemp(`${outputDirectory}.tmp-`);
58
+ try {
59
+ const extractedArchive = join(staging, ".docker-archive");
60
+ await extractPortableTar(join(releaseRoot, "images.tar"), extractedArchive);
61
+ const preparedRelease = join(staging, ".prepared-release");
62
+ await prepareDeterministicRelease(
63
+ releaseRoot,
64
+ extractedArchive,
65
+ preparedRelease,
66
+ );
67
+ const fallback = await createFullBundleFallback(
68
+ preparedRelease,
69
+ join(staging, "full-bundle.tar.gz"),
70
+ );
71
+ const manifest = await buildDeliveryManifest({
72
+ archiveRoot: extractedArchive,
73
+ releaseRoot: preparedRelease,
74
+ storeRoot: staging,
75
+ product: required(options, "product"),
76
+ releaseId: required(options, "release-id"),
77
+ gitSha: required(options, "git-sha"),
78
+ platform: parsePlatform(required(options, "platform")),
79
+ createdAt: required(options, "created-at"),
80
+ images: await readImages(requiredPath(options, "images-json")),
81
+ fullBundle: { ...fallback, file: "full-bundle.tar.gz" },
82
+ });
83
+ await Promise.all([
84
+ rm(extractedArchive, { recursive: true, force: true }),
85
+ rm(preparedRelease, { recursive: true, force: true }),
86
+ ]);
87
+ const digest = await writeManifest(
88
+ join(staging, "delivery-manifest.v2.json"),
89
+ manifest,
90
+ );
91
+ await writeFile(join(staging, "DELIVERY-MANIFEST.sha256"), `${digest}\n`, {
92
+ flag: "wx",
93
+ mode: 0o644,
94
+ });
95
+ await rename(staging, outputDirectory);
96
+ output({
97
+ outputDirectory,
98
+ manifestDigest: digest,
99
+ fullBundle: manifest.compatibility.fullBundle,
100
+ totalBytes: manifest.blobs.reduce((total, blob) => total + blob.size, 0),
101
+ totalBlobs: manifest.blobs.length,
102
+ });
103
+ } catch (error) {
104
+ await rm(staging, { recursive: true, force: true });
105
+ throw error;
106
+ }
107
+ } else if (command === "sync-ssh") {
108
+ const options = parseOptions(argv);
109
+ const manifest = await readManifest(
110
+ requiredPath(options, "manifest"),
111
+ assertSha256Digest(required(options, "manifest-digest")),
112
+ );
113
+ const preference = parsePreference(options.get("mode"));
114
+ const result = await syncReleaseOverSsh({
115
+ manifest,
116
+ localStoreRoot: requiredPath(options, "store"),
117
+ transport: new SshProcessTransport({
118
+ target: required(options, "target"),
119
+ remoteBinary: required(options, "remote-binary"),
120
+ remoteStore: required(options, "remote-store"),
121
+ remoteInstallRoot: required(options, "remote-install-root"),
122
+ remoteFallbackRoot: required(options, "remote-fallback-root"),
123
+ }),
124
+ ownerToken: randomBytes(24).toString("hex"),
125
+ preference,
126
+ fallbackPath: options.has("fallback")
127
+ ? requiredPath(options, "fallback")
128
+ : undefined,
129
+ });
130
+ output(result);
131
+ } else if (command === "capabilities") {
132
+ output(capabilities);
133
+ } else if (command === "pull-registry") {
134
+ const options = parseOptions(argv);
135
+ const manifest = validateManifest(await readJsonStdin());
136
+ const digest = assertSha256Digest(required(options, "manifest-digest"));
137
+ if (manifestDigest(manifest) !== digest) {
138
+ throw new Error("registry pull 前 manifest digest 不匹配");
139
+ }
140
+ output(await pullRegistryRelease(manifest));
141
+ } else if (command === "lock") {
142
+ const options = parseOptions(argv);
143
+ const lock = await acquireDeliveryLock(
144
+ requiredPath(options, "install-root"),
145
+ required(options, "owner-token"),
146
+ );
147
+ output({ locked: true, directory: lock.directory });
148
+ } else if (command === "unlock") {
149
+ const options = parseOptions(argv);
150
+ await releaseDeliveryLock(lockFromOptions(options));
151
+ output({ unlocked: true });
152
+ } else if (command === "inventory") {
153
+ const options = parseOptions(argv);
154
+ const blobs = parseBlobs(await readJsonStdin());
155
+ const present = await inventoryBlobs(requiredPath(options, "store"), blobs);
156
+ output({ present: [...present].sort() });
157
+ } else if (command === "preflight") {
158
+ const options = parseOptions(argv);
159
+ const manifest = validateManifest(await readJsonStdin());
160
+ const lock = lockFromOptions(options);
161
+ await verifyDeliveryLock(lock, requiredPath(options, "install-root"));
162
+ const present = await inventoryBlobs(
163
+ requiredPath(options, "store"),
164
+ manifest.blobs,
165
+ );
166
+ output(
167
+ await preflightReleaseSpace(
168
+ requiredPath(options, "install-root"),
169
+ requiredPath(options, "store"),
170
+ manifest,
171
+ present,
172
+ optionalInteger(options, "reserve-bytes"),
173
+ ),
174
+ );
175
+ } else if (command === "partial") {
176
+ const options = parseOptions(argv);
177
+ const blob = blobFromOptions(options);
178
+ output({
179
+ offset: await partialBlobSize(requiredPath(options, "store"), blob),
180
+ });
181
+ } else if (command === "receive") {
182
+ const options = parseOptions(argv);
183
+ const result = await receiveBlobStream({
184
+ storeRoot: requiredPath(options, "store"),
185
+ blob: blobFromOptions(options),
186
+ offset: integer(options, "offset"),
187
+ chunks: stdinChunks(),
188
+ });
189
+ output(result);
190
+ } else if (command === "stage") {
191
+ const options = parseOptions(argv);
192
+ const manifest = validateManifest(await readJsonStdin());
193
+ const directory = await stageRelease({
194
+ installRoot: requiredPath(options, "install-root"),
195
+ storeRoot: requiredPath(options, "store"),
196
+ manifest,
197
+ expectedManifestDigest: assertSha256Digest(
198
+ required(options, "manifest-digest"),
199
+ ),
200
+ lock: lockFromOptions(options),
201
+ reserveBytes: optionalInteger(options, "reserve-bytes"),
202
+ });
203
+ output({ staged: true, directory });
204
+ } else {
205
+ throw new Error(
206
+ "用法:windy-delivery pack|sync-ssh|capabilities|pull-registry|lock|unlock|inventory|preflight|partial|receive|stage",
207
+ );
208
+ }
209
+
210
+ function parseOptions(argv: readonly string[]): Map<string, string> {
211
+ const output = new Map<string, string>();
212
+ for (let index = 0; index < argv.length; index += 2) {
213
+ const key = argv[index];
214
+ const value = argv[index + 1];
215
+ if (
216
+ !key?.startsWith("--") ||
217
+ value === undefined ||
218
+ value.startsWith("--")
219
+ ) {
220
+ throw new Error("命令参数必须使用 --name value");
221
+ }
222
+ const name = key.slice(2);
223
+ if (output.has(name)) throw new Error(`参数重复:--${name}`);
224
+ output.set(name, value);
225
+ }
226
+ return output;
227
+ }
228
+
229
+ function required(options: ReadonlyMap<string, string>, name: string): string {
230
+ const value = options.get(name);
231
+ if (!value) throw new Error(`缺少 --${name}`);
232
+ return value;
233
+ }
234
+
235
+ function requiredPath(
236
+ options: ReadonlyMap<string, string>,
237
+ name: string,
238
+ ): string {
239
+ return resolve(required(options, name));
240
+ }
241
+
242
+ function integer(options: ReadonlyMap<string, string>, name: string): number {
243
+ const value = Number(required(options, name));
244
+ if (!Number.isSafeInteger(value) || value < 0)
245
+ throw new Error(`--${name} 必须是非负整数`);
246
+ return value;
247
+ }
248
+
249
+ function optionalInteger(
250
+ options: ReadonlyMap<string, string>,
251
+ name: string,
252
+ ): number | undefined {
253
+ return options.has(name) ? integer(options, name) : undefined;
254
+ }
255
+
256
+ function blobFromOptions(options: ReadonlyMap<string, string>): ContentBlob {
257
+ return {
258
+ digest: assertSha256Digest(required(options, "digest")),
259
+ size: integer(options, "size"),
260
+ };
261
+ }
262
+
263
+ function parsePreference(
264
+ value: string | undefined,
265
+ ): "auto" | "blob-delta" | "full-bundle" {
266
+ if (value === undefined || value === "auto") return "auto";
267
+ if (value === "blob-delta" || value === "full-bundle") return value;
268
+ throw new Error("--mode 仅支持 auto、blob-delta 或 full-bundle");
269
+ }
270
+
271
+ function parsePlatform(value: string): ReleasePlatform {
272
+ if (value === "linux/amd64" || value === "linux/arm64") return value;
273
+ throw new Error("--platform 仅支持 linux/amd64 或 linux/arm64");
274
+ }
275
+
276
+ async function readImages(path: string): Promise<ReleaseImageV2[]> {
277
+ const value = JSON.parse(await readFile(path, "utf8"));
278
+ if (!Array.isArray(value) || value.length === 0) {
279
+ throw new Error("--images-json 必须是非空数组");
280
+ }
281
+ return value as ReleaseImageV2[];
282
+ }
283
+
284
+ function lockFromOptions(options: ReadonlyMap<string, string>): DeliveryLock {
285
+ const installRoot = requiredPath(options, "install-root");
286
+ return {
287
+ directory: resolve(installRoot, ".delivery-lock"),
288
+ ownerToken: required(options, "owner-token"),
289
+ };
290
+ }
291
+
292
+ function parseBlobs(value: unknown): ContentBlob[] {
293
+ if (!Array.isArray(value)) throw new Error("stdin 必须是 blob 数组");
294
+ return value.map((item) => {
295
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
296
+ throw new Error("blob 必须是对象");
297
+ }
298
+ const record = item as Record<string, unknown>;
299
+ if (
300
+ typeof record.size !== "number" ||
301
+ !Number.isSafeInteger(record.size) ||
302
+ record.size < 0
303
+ ) {
304
+ throw new Error("blob size 无效");
305
+ }
306
+ return { digest: assertSha256Digest(record.digest), size: record.size };
307
+ });
308
+ }
309
+
310
+ async function readJsonStdin(): Promise<unknown> {
311
+ const text = await Bun.stdin.text();
312
+ if (!text.trim()) throw new Error("stdin 缺少 JSON");
313
+ return JSON.parse(text);
314
+ }
315
+
316
+ async function* stdinChunks(): AsyncIterable<Uint8Array> {
317
+ const reader = Bun.stdin.stream().getReader();
318
+ try {
319
+ while (true) {
320
+ const result = await reader.read();
321
+ if (result.done) return;
322
+ yield result.value;
323
+ }
324
+ } finally {
325
+ reader.releaseLock();
326
+ }
327
+ }
328
+
329
+ function output(value: unknown): void {
330
+ console.info(JSON.stringify(value));
331
+ }