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,167 @@
1
+ import { blobPath, inventoryBlobs } from "./blob-store.js";
2
+ import { sha256File } from "./digest.js";
3
+ import { stat } from "node:fs/promises";
4
+ import { manifestDigest, validateManifest } from "./manifest.js";
5
+ import { negotiateDelivery, type DeliverySelection } from "./negotiation.js";
6
+ import { planBlobTransfer } from "./transfer.js";
7
+ import type {
8
+ ContentBlob,
9
+ DeliveryCapabilities,
10
+ DeliveryManifestV2,
11
+ Sha256Digest,
12
+ TransferStatistics,
13
+ } from "./types.js";
14
+
15
+ export interface SshDeliveryTransport {
16
+ capabilities(): Promise<DeliveryCapabilities | undefined>;
17
+ lock(ownerToken: string): Promise<void>;
18
+ unlock(ownerToken: string): Promise<void>;
19
+ inventory(blobs: readonly ContentBlob[]): Promise<Set<Sha256Digest>>;
20
+ preflight(manifest: DeliveryManifestV2, ownerToken: string): Promise<void>;
21
+ preflightFallback(
22
+ fallback: NonNullable<DeliveryManifestV2["compatibility"]["fullBundle"]>,
23
+ ownerToken: string,
24
+ ): Promise<void>;
25
+ partial(blob: ContentBlob): Promise<number>;
26
+ upload(blob: ContentBlob, offset: number, localPath: string): Promise<number>;
27
+ stage(
28
+ manifest: DeliveryManifestV2,
29
+ digest: Sha256Digest,
30
+ ownerToken: string,
31
+ ): Promise<string>;
32
+ uploadFallback(
33
+ fallback: NonNullable<DeliveryManifestV2["compatibility"]["fullBundle"]>,
34
+ localPath: string,
35
+ ownerToken: string,
36
+ ): Promise<{ remotePath: string; uploaded: boolean }>;
37
+ }
38
+
39
+ export type SshSyncResult =
40
+ | {
41
+ mode: "blob-delta";
42
+ stagedDirectory: string;
43
+ statistics: TransferStatistics;
44
+ }
45
+ | {
46
+ mode: "full-bundle-required";
47
+ selection: Extract<DeliverySelection, { mode: "full-bundle" }>;
48
+ remoteArchive?: string;
49
+ statistics?: TransferStatistics;
50
+ }
51
+ | {
52
+ mode: "registry-preferred";
53
+ selection: Extract<DeliverySelection, { mode: "registry" }>;
54
+ };
55
+
56
+ export async function syncReleaseOverSsh(input: {
57
+ manifest: DeliveryManifestV2;
58
+ localStoreRoot: string;
59
+ transport: SshDeliveryTransport;
60
+ ownerToken: string;
61
+ preference?: "auto" | "blob-delta" | "full-bundle";
62
+ fallbackPath?: string;
63
+ }): Promise<SshSyncResult> {
64
+ const manifest = validateManifest(input.manifest);
65
+ const capabilities = await input.transport.capabilities();
66
+ const selection = negotiateDelivery(
67
+ manifest,
68
+ capabilities ?? legacyCapabilities,
69
+ input.preference ?? "auto",
70
+ );
71
+ if (selection.mode === "registry") {
72
+ return { mode: "registry-preferred", selection };
73
+ }
74
+ if (selection.mode === "full-bundle") {
75
+ if (!input.fallbackPath || !manifest.compatibility.fullBundle) {
76
+ return { mode: "full-bundle-required", selection };
77
+ }
78
+ const fallback = manifest.compatibility.fullBundle;
79
+ const info = await stat(input.fallbackPath);
80
+ if (
81
+ info.size !== fallback.size ||
82
+ (await sha256File(input.fallbackPath)) !== fallback.digest
83
+ ) {
84
+ throw new Error("本地完整 bundle fallback 摘要或尺寸不匹配");
85
+ }
86
+ await input.transport.lock(input.ownerToken);
87
+ try {
88
+ await input.transport.preflightFallback(fallback, input.ownerToken);
89
+ const transferred = await input.transport.uploadFallback(
90
+ fallback,
91
+ input.fallbackPath,
92
+ input.ownerToken,
93
+ );
94
+ return {
95
+ mode: "full-bundle-required",
96
+ selection,
97
+ remoteArchive: transferred.remotePath,
98
+ statistics: {
99
+ totalBytes: fallback.size,
100
+ reusedBytes: transferred.uploaded ? 0 : fallback.size,
101
+ uploadedBytes: transferred.uploaded ? fallback.size : 0,
102
+ totalBlobs: 1,
103
+ reusedBlobs: transferred.uploaded ? 0 : 1,
104
+ uploadedBlobs: transferred.uploaded ? 1 : 0,
105
+ },
106
+ };
107
+ } finally {
108
+ await input.transport.unlock(input.ownerToken);
109
+ }
110
+ }
111
+ const localPresent = await inventoryBlobs(
112
+ input.localStoreRoot,
113
+ manifest.blobs,
114
+ );
115
+ if (localPresent.size !== manifest.blobs.length) {
116
+ throw new Error("本地 release blob store 不完整或损坏");
117
+ }
118
+ await input.transport.lock(input.ownerToken);
119
+ try {
120
+ const present = await input.transport.inventory(manifest.blobs);
121
+ await input.transport.preflight(manifest, input.ownerToken);
122
+ const plan = planBlobTransfer(manifest.blobs, present);
123
+ let uploadedBytes = 0;
124
+ let uploadedBlobs = 0;
125
+ for (const blob of plan.missing) {
126
+ const offset = await input.transport.partial(blob);
127
+ if (offset < 0 || offset > blob.size) {
128
+ throw new Error(`远端返回无效 blob offset:${blob.digest}`);
129
+ }
130
+ if (offset < blob.size) {
131
+ uploadedBytes += await input.transport.upload(
132
+ blob,
133
+ offset,
134
+ blobPath(input.localStoreRoot, blob.digest),
135
+ );
136
+ uploadedBlobs += 1;
137
+ }
138
+ }
139
+ const totalBytes = plan.statistics.totalBytes;
140
+ const stagedDirectory = await input.transport.stage(
141
+ manifest,
142
+ manifestDigest(manifest),
143
+ input.ownerToken,
144
+ );
145
+ return {
146
+ mode: "blob-delta",
147
+ stagedDirectory,
148
+ statistics: {
149
+ totalBytes,
150
+ reusedBytes: totalBytes - uploadedBytes,
151
+ uploadedBytes,
152
+ totalBlobs: plan.statistics.totalBlobs,
153
+ reusedBlobs: plan.statistics.totalBlobs - uploadedBlobs,
154
+ uploadedBlobs,
155
+ },
156
+ };
157
+ } finally {
158
+ await input.transport.unlock(input.ownerToken);
159
+ }
160
+ }
161
+
162
+ const legacyCapabilities: DeliveryCapabilities = {
163
+ schemaVersions: [1],
164
+ registryByDigest: false,
165
+ blobDelta: false,
166
+ resumableBlobTransfer: false,
167
+ };
@@ -0,0 +1,241 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ mkdir,
4
+ readFile,
5
+ rename,
6
+ rm,
7
+ stat,
8
+ statfs,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { createDeterministicTar } from "./archive.js";
13
+ import { blobPath, inventoryBlobs, materializeArchive } from "./blob-store.js";
14
+ import { releaseChecksumFile, verifyReleaseChecksums } from "./checksums.js";
15
+ import { canonicalManifestJson, manifestDigest } from "./manifest.js";
16
+ import type { DeliveryManifestV2, Sha256Digest } from "./types.js";
17
+
18
+ export interface DeliveryLock {
19
+ directory: string;
20
+ ownerToken: string;
21
+ }
22
+
23
+ export async function acquireDeliveryLock(
24
+ installRoot: string,
25
+ ownerToken = randomBytes(24).toString("hex"),
26
+ ): Promise<DeliveryLock> {
27
+ if (!/^[a-f0-9]{48}$/.test(ownerToken))
28
+ throw new Error("lock owner token 无效");
29
+ const directory = join(resolve(installRoot), ".delivery-lock");
30
+ await mkdir(dirname(directory), { recursive: true });
31
+ try {
32
+ await mkdir(directory);
33
+ await writeFile(join(directory, "owner"), `${ownerToken}\n`, {
34
+ flag: "wx",
35
+ mode: 0o600,
36
+ });
37
+ return { directory, ownerToken };
38
+ } catch (error) {
39
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") {
40
+ throw new Error("已有并发交付持有安装锁", { cause: error });
41
+ }
42
+ await rm(directory, { recursive: true, force: true });
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ export async function releaseDeliveryLock(lock: DeliveryLock): Promise<void> {
48
+ const owner = (await readFile(join(lock.directory, "owner"), "utf8")).trim();
49
+ if (owner !== lock.ownerToken)
50
+ throw new Error("交付锁 owner 不匹配,拒绝释放");
51
+ await rm(lock.directory, { recursive: true, force: false });
52
+ }
53
+
54
+ export async function stageRelease(input: {
55
+ installRoot: string;
56
+ storeRoot: string;
57
+ manifest: DeliveryManifestV2;
58
+ expectedManifestDigest: Sha256Digest;
59
+ lock: DeliveryLock;
60
+ reserveBytes?: number;
61
+ }): Promise<string> {
62
+ const installRoot = resolve(input.installRoot);
63
+ await verifyDeliveryLock(input.lock, installRoot);
64
+ if (manifestDigest(input.manifest) !== input.expectedManifestDigest) {
65
+ throw new Error("stage 前 manifest digest 校验失败");
66
+ }
67
+ const present = await inventoryBlobs(input.storeRoot, input.manifest.blobs);
68
+ if (present.size !== input.manifest.blobs.length) {
69
+ throw new Error("远端 blob 不完整或损坏,拒绝 stage release");
70
+ }
71
+ await preflightReleaseSpace(
72
+ installRoot,
73
+ input.storeRoot,
74
+ input.manifest,
75
+ present,
76
+ input.reserveBytes,
77
+ );
78
+ const stagingRoot = join(installRoot, ".staging");
79
+ const releasesRoot = join(installRoot, "releases");
80
+ const nonce = randomBytes(12).toString("hex");
81
+ const stage = join(stagingRoot, `${input.manifest.releaseId}.${nonce}`);
82
+ const final = join(releasesRoot, input.manifest.releaseId);
83
+ await mkdir(stagingRoot, { recursive: true });
84
+ await mkdir(releasesRoot, { recursive: true });
85
+ try {
86
+ await mkdir(stage);
87
+ const content = join(stage, "release");
88
+ if (input.manifest.releaseFiles.length > 0) {
89
+ await materializeArchive(
90
+ input.storeRoot,
91
+ content,
92
+ input.manifest.releaseFiles,
93
+ );
94
+ } else {
95
+ await mkdir(content);
96
+ }
97
+ await createDeterministicTar(
98
+ join(content, "images.tar"),
99
+ input.manifest.dockerArchive.entries.map((entry) => ({
100
+ source: blobPath(input.storeRoot, entry.digest),
101
+ path: entry.path,
102
+ mode: entry.mode,
103
+ })),
104
+ );
105
+ if (
106
+ input.manifest.releaseFiles.some(
107
+ ({ path }) => path === releaseChecksumFile,
108
+ )
109
+ ) {
110
+ await verifyReleaseChecksums(content, [
111
+ ...input.manifest.releaseFiles.map(({ path }) => path),
112
+ "images.tar",
113
+ ]);
114
+ }
115
+ await writeFile(
116
+ join(stage, "delivery-manifest.v2.json"),
117
+ canonicalManifestJson(input.manifest),
118
+ { flag: "wx", mode: 0o644 },
119
+ );
120
+ await writeFile(
121
+ join(stage, "DELIVERY-MANIFEST.sha256"),
122
+ `${input.expectedManifestDigest}\n`,
123
+ { flag: "wx", mode: 0o644 },
124
+ );
125
+ try {
126
+ await rename(stage, final);
127
+ } catch (error) {
128
+ if (
129
+ !new Set(["EEXIST", "ENOTEMPTY"]).has(
130
+ (error as NodeJS.ErrnoException).code ?? "",
131
+ )
132
+ ) {
133
+ throw error;
134
+ }
135
+ const digest = (
136
+ await readFile(join(final, "DELIVERY-MANIFEST.sha256"), "utf8")
137
+ ).trim();
138
+ if (digest !== input.expectedManifestDigest) {
139
+ throw new Error("同 releaseId 已存在不同 manifest,拒绝覆盖", {
140
+ cause: error,
141
+ });
142
+ }
143
+ await rm(stage, { recursive: true, force: true });
144
+ }
145
+ return join(final, "release");
146
+ } catch (error) {
147
+ await rm(stage, { recursive: true, force: true });
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ export async function cleanupStaging(installRoot: string, lock: DeliveryLock) {
153
+ const root = resolve(installRoot);
154
+ await verifyDeliveryLock(lock, root);
155
+ await rm(join(root, ".staging"), { recursive: true, force: true });
156
+ }
157
+
158
+ export async function preflightReleaseSpace(
159
+ installRoot: string,
160
+ storeRoot: string,
161
+ manifest: DeliveryManifestV2,
162
+ present: ReadonlySet<Sha256Digest>,
163
+ reserveBytes = 512 * 1024 * 1024,
164
+ ): Promise<{
165
+ storeAvailableBytes: number;
166
+ storeRequiredBytes: number;
167
+ installAvailableBytes: number;
168
+ installRequiredBytes: number;
169
+ }> {
170
+ const missingBytes = manifest.blobs
171
+ .filter(({ digest }) => !present.has(digest))
172
+ .reduce((total, { size }) => total + size, 0);
173
+ const releaseBytes = manifest.releaseFiles.reduce(
174
+ (total, { size }) => total + size,
175
+ 0,
176
+ );
177
+ const metadataBytes = Buffer.byteLength(canonicalManifestJson(manifest)) + 80;
178
+ const stageBytes = archiveSizeBudget(manifest) + releaseBytes + metadataBytes;
179
+ if (
180
+ !Number.isSafeInteger(missingBytes + stageBytes + reserveBytes) ||
181
+ reserveBytes < 0
182
+ ) {
183
+ throw new Error("交付空间预算超出安全范围");
184
+ }
185
+ const resolvedInstall = resolve(installRoot);
186
+ const resolvedStore = resolve(storeRoot);
187
+ await mkdir(resolvedStore, { recursive: true });
188
+ const [installFile, storeFile, installFs, storeFs] = await Promise.all([
189
+ stat(resolvedInstall),
190
+ stat(resolvedStore),
191
+ statfs(resolvedInstall),
192
+ statfs(resolvedStore),
193
+ ]);
194
+ const sameFilesystem = installFile.dev === storeFile.dev;
195
+ const installAvailableBytes = installFs.bavail * installFs.bsize;
196
+ const storeAvailableBytes = storeFs.bavail * storeFs.bsize;
197
+ const storeRequiredBytes = sameFilesystem
198
+ ? missingBytes + stageBytes + reserveBytes
199
+ : missingBytes + reserveBytes;
200
+ const installRequiredBytes = sameFilesystem
201
+ ? storeRequiredBytes
202
+ : stageBytes + reserveBytes;
203
+ if (storeAvailableBytes < storeRequiredBytes) {
204
+ throw new Error(
205
+ `blob store 空间不足:需要 ${storeRequiredBytes},可用 ${storeAvailableBytes}`,
206
+ );
207
+ }
208
+ if (!sameFilesystem && installAvailableBytes < installRequiredBytes) {
209
+ throw new Error(
210
+ `release stage 空间不足:需要 ${installRequiredBytes},可用 ${installAvailableBytes}`,
211
+ );
212
+ }
213
+ return {
214
+ storeAvailableBytes,
215
+ storeRequiredBytes,
216
+ installAvailableBytes,
217
+ installRequiredBytes,
218
+ };
219
+ }
220
+
221
+ function archiveSizeBudget(manifest: DeliveryManifestV2): number {
222
+ const bytes = manifest.dockerArchive.entries.reduce(
223
+ (total, entry) =>
224
+ total + 512 + entry.size + ((512 - (entry.size % 512)) % 512),
225
+ 1024,
226
+ );
227
+ if (!Number.isSafeInteger(bytes))
228
+ throw new Error("archive 空间预算超出安全范围");
229
+ return bytes;
230
+ }
231
+
232
+ export async function verifyDeliveryLock(
233
+ lock: DeliveryLock,
234
+ installRoot: string,
235
+ ): Promise<void> {
236
+ if (resolve(lock.directory) !== join(installRoot, ".delivery-lock")) {
237
+ throw new Error("交付锁不属于目标安装目录");
238
+ }
239
+ const owner = (await readFile(join(lock.directory, "owner"), "utf8")).trim();
240
+ if (owner !== lock.ownerToken) throw new Error("交付锁 owner 不匹配");
241
+ }
@@ -0,0 +1,54 @@
1
+ import type { ContentBlob, Sha256Digest, TransferStatistics } from "./types.js";
2
+
3
+ export interface BlobTransferPlan {
4
+ missing: ContentBlob[];
5
+ statistics: TransferStatistics;
6
+ }
7
+
8
+ export function planBlobTransfer(
9
+ blobs: readonly ContentBlob[],
10
+ present: ReadonlySet<Sha256Digest>,
11
+ ): BlobTransferPlan {
12
+ const unique = new Map<Sha256Digest, ContentBlob>();
13
+ for (const blob of blobs) {
14
+ const previous = unique.get(blob.digest);
15
+ if (previous && previous.size !== blob.size) {
16
+ throw new Error(`同一 blob digest 声明了不同尺寸:${blob.digest}`);
17
+ }
18
+ unique.set(blob.digest, blob);
19
+ }
20
+ const all = [...unique.values()].sort((left, right) =>
21
+ left.digest.localeCompare(right.digest),
22
+ );
23
+ const missing = all.filter(({ digest }) => !present.has(digest));
24
+ const totalBytes = sum(all.map(({ size }) => size));
25
+ const uploadedBytes = sum(missing.map(({ size }) => size));
26
+ return {
27
+ missing,
28
+ statistics: {
29
+ totalBytes,
30
+ reusedBytes: totalBytes - uploadedBytes,
31
+ uploadedBytes,
32
+ totalBlobs: all.length,
33
+ reusedBlobs: all.length - missing.length,
34
+ uploadedBlobs: missing.length,
35
+ },
36
+ };
37
+ }
38
+
39
+ export function formatTransferStatistics(stats: TransferStatistics): string {
40
+ return [
41
+ `总字节=${stats.totalBytes}`,
42
+ `复用字节=${stats.reusedBytes}`,
43
+ `实际上传字节=${stats.uploadedBytes}`,
44
+ `blob=${stats.totalBlobs}`,
45
+ `复用blob=${stats.reusedBlobs}`,
46
+ `上传blob=${stats.uploadedBlobs}`,
47
+ ].join(" ");
48
+ }
49
+
50
+ function sum(values: readonly number[]): number {
51
+ const total = values.reduce((result, value) => result + value, 0);
52
+ if (!Number.isSafeInteger(total)) throw new Error("传输字节统计超出安全范围");
53
+ return total;
54
+ }
@@ -0,0 +1,78 @@
1
+ export type ReleasePlatform = "linux/amd64" | "linux/arm64";
2
+ export type Sha256Digest = `sha256:${string}`;
3
+
4
+ export interface ContentBlob {
5
+ digest: Sha256Digest;
6
+ size: number;
7
+ }
8
+
9
+ export interface ArchiveEntry extends ContentBlob {
10
+ path: string;
11
+ mode: number;
12
+ }
13
+
14
+ export interface ReleaseImageV2 {
15
+ role: string;
16
+ platform: ReleasePlatform;
17
+ configDigest: Sha256Digest;
18
+ registry?: {
19
+ repository: string;
20
+ digest: Sha256Digest;
21
+ layers: ContentBlob[];
22
+ };
23
+ }
24
+
25
+ export interface FullBundleFallback {
26
+ schemaVersion: 1;
27
+ file: string;
28
+ digest: Sha256Digest;
29
+ size: number;
30
+ }
31
+
32
+ export interface DeliveryManifestV2 {
33
+ schemaVersion: 2;
34
+ minimumReaderVersion: 2;
35
+ product: string;
36
+ releaseId: string;
37
+ gitSha: string;
38
+ platform: ReleasePlatform;
39
+ createdAt: string;
40
+ images: ReleaseImageV2[];
41
+ dockerArchive: {
42
+ format: "oci-image-layout-v1";
43
+ entries: ArchiveEntry[];
44
+ };
45
+ releaseFiles: ArchiveEntry[];
46
+ blobs: ContentBlob[];
47
+ compatibility: {
48
+ fullBundle?: FullBundleFallback;
49
+ };
50
+ }
51
+
52
+ export interface DeliveryCapabilities {
53
+ schemaVersions: readonly number[];
54
+ registryByDigest: boolean;
55
+ blobDelta: boolean;
56
+ resumableBlobTransfer: boolean;
57
+ }
58
+
59
+ export interface TransferStatistics {
60
+ totalBytes: number;
61
+ reusedBytes: number;
62
+ uploadedBytes: number;
63
+ totalBlobs: number;
64
+ reusedBlobs: number;
65
+ uploadedBlobs: number;
66
+ }
67
+
68
+ export interface RegistryTransferStatistics {
69
+ totalBytes: number;
70
+ reusedBytes: number;
71
+ uploadedBytes: null;
72
+ descriptorPulledBytes: number;
73
+ totalBlobs: number;
74
+ reusedBlobs: number;
75
+ uploadedBlobs: null;
76
+ descriptorPulledBlobs: number;
77
+ measurement: "docker-layer-status";
78
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "lib": ["esnext"],
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "noImplicitAny": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "outDir": "dist",
12
+ "types": ["bun"]
13
+ },
14
+ "include": ["src/**/*", "index.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }