@actiondock/builder 2.0.11-beta.0 → 2.0.12

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.
@@ -0,0 +1,26 @@
1
+ import type { BuildPlan, BuildPlannerOptions } from "./types";
2
+ /**
3
+ * 构建规划器。
4
+ * 纯声明式解析 actiondock.manifest.json 与配置,绝不执行 Action 业务代码。
5
+ */
6
+ export declare class BuildPlanner {
7
+ private projectRoot;
8
+ constructor(options?: {
9
+ projectRoot?: string;
10
+ });
11
+ /**
12
+ * 执行依赖闭包裁剪与构建规划。
13
+ *
14
+ * @param options 规划参数
15
+ * @returns 完整的 BuildPlan 结构
16
+ */
17
+ plan(options?: BuildPlannerOptions): BuildPlan;
18
+ /**
19
+ * 静态辅助调用方法。
20
+ */
21
+ static plan(options: BuildPlannerOptions): BuildPlan;
22
+ }
23
+ /**
24
+ * 快捷构建规划函数。
25
+ */
26
+ export declare function buildPlan(options: BuildPlannerOptions): BuildPlan;
@@ -0,0 +1,311 @@
1
+ import type { ActionDockManifest, ProjectConfig } from "@actiondock/core";
2
+ /**
3
+ * Action 依赖描述。
4
+ */
5
+ export interface ActionDependency {
6
+ /** Action 唯一标识符 */
7
+ id: string;
8
+ /** Action 入口文件相对路径 */
9
+ entry: string;
10
+ /** Action 源码文件绝对路径 */
11
+ resolvedPath: string;
12
+ /** 静态依赖的下游 Action ID 列表 */
13
+ uses: string[];
14
+ /** Action 功能描述 */
15
+ description?: string;
16
+ /** 入参校验模式 */
17
+ inputSchema?: Record<string, unknown> | boolean;
18
+ /** 出参校验模式 */
19
+ outputSchema?: Record<string, unknown> | boolean;
20
+ /** 标签列表 */
21
+ tags?: string[];
22
+ /** 扩展注解 */
23
+ annotations?: Record<string, unknown>;
24
+ }
25
+ /**
26
+ * 资产或模块依赖描述。
27
+ */
28
+ export interface AssetDependency {
29
+ /** 资产相对路径 */
30
+ path: string;
31
+ /** 资产绝对物理路径 */
32
+ resolvedPath: string;
33
+ /** 资产类型:静态资产、规程文档、配置文件或模块源码 */
34
+ type: "asset" | "playbook" | "config" | "module";
35
+ }
36
+ /**
37
+ * 外部依赖描述。
38
+ */
39
+ export interface ExternalDependency {
40
+ /** 外部依赖包名 */
41
+ name: string;
42
+ /** 版本约束范围 */
43
+ versionRange?: string;
44
+ /** 是否为开发期依赖 */
45
+ isDev?: boolean;
46
+ }
47
+ /**
48
+ * 构建规划中的依赖集合分类。
49
+ */
50
+ export interface BuildPlanDependencies {
51
+ /** Action 依赖闭包 */
52
+ actions: ActionDependency[];
53
+ /** 模块与资产依赖 */
54
+ modulesAndAssets: AssetDependency[];
55
+ /** 外部 npm 依赖 */
56
+ external: ExternalDependency[];
57
+ }
58
+ /**
59
+ * Playbook 规划项。
60
+ */
61
+ export interface PlaybookPlanEntry {
62
+ id: string;
63
+ filePath: string;
64
+ actions?: string[];
65
+ description?: string;
66
+ }
67
+ /**
68
+ * 构建规划产物结构。
69
+ */
70
+ export interface BuildPlan {
71
+ /** 所属 Package ID */
72
+ packageId: string;
73
+ /** 项目展示名称 */
74
+ packageName: string;
75
+ /** 版本号 */
76
+ version: string;
77
+ /** 描述信息 */
78
+ description?: string;
79
+ /** 项目根目录绝对路径 */
80
+ projectRoot: string;
81
+ /** actions 目录相对路径 */
82
+ actionsDir?: string;
83
+ /** playbooks 目录相对路径 */
84
+ playbooksDir?: string;
85
+ /** 参与构建的 Action 列表 */
86
+ actions: ActionDependency[];
87
+ /** 参与构建的 Playbook 列表 */
88
+ playbooks: PlaybookPlanEntry[];
89
+ /** 分类依赖明细 */
90
+ dependencies: BuildPlanDependencies;
91
+ /** 资产路径列表 */
92
+ assets: string[];
93
+ /** 声明的配置定义字典 */
94
+ configDefs?: Record<string, unknown>;
95
+ /** 规划元数据 */
96
+ metadata: {
97
+ plannedAt: string;
98
+ schemaVersion: number;
99
+ actionCount: number;
100
+ playbookCount: number;
101
+ };
102
+ }
103
+ /**
104
+ * 构建规划器配置选项。
105
+ */
106
+ export interface BuildPlannerOptions {
107
+ /** 项目根目录绝对路径 */
108
+ projectRoot: string;
109
+ /** 显式传入的项目配置(若未提供则从 actiondock.json 读取) */
110
+ config?: ProjectConfig;
111
+ /** 显式传入的声明式清单(若未提供则从 actiondock.manifest.json 读取) */
112
+ manifest?: ActionDockManifest;
113
+ /** 挑选的 Action ID 列表(用于依赖闭包裁剪) */
114
+ actions?: string[];
115
+ /** 挑选的 Playbook ID 列表(用于 Playbook 驱动的依赖闭包裁剪) */
116
+ playbooks?: string[];
117
+ }
118
+ /**
119
+ * 支持的 Bun 独立编译目标平台。
120
+ */
121
+ export type CompileTarget = "bun" | "host" | "bun-linux-x64" | "linux-x64" | "bun-linux-arm64" | "linux-arm64" | "bun-darwin-x64" | "darwin-x64" | "bun-darwin-arm64" | "darwin-arm64" | "bun-windows-x64" | "windows-x64" | "bun-linux-x64-baseline" | "linux-x64-baseline" | "bun-linux-x64-modern" | "linux-x64-modern";
122
+ /**
123
+ * Bun 独立编译器参数选项。
124
+ */
125
+ export interface BunCompilerOptions {
126
+ /** 编译入口文件绝对路径 */
127
+ entrypoint: string;
128
+ /** 目标二进制文件输出路径 */
129
+ outfile: string;
130
+ /** 编译目标平台架构(默认 host) */
131
+ target?: CompileTarget | string;
132
+ /** 是否开启代码压缩混淆(默认 true) */
133
+ minify?: boolean;
134
+ /** 是否启用字节码预编译(默认 true) */
135
+ bytecode?: boolean;
136
+ /** 构建工作目录(默认入口文件所在根目录) */
137
+ cwd?: string;
138
+ /** 自定义环境变量 */
139
+ env?: Record<string, string>;
140
+ /** 所属 Package ID(写入元数据用) */
141
+ packageId?: string;
142
+ /** 所属版本号(写入元数据用) */
143
+ version?: string;
144
+ /** 包含的 Action ID 列表(写入元数据用) */
145
+ actions?: string[];
146
+ /** 是否生成 artifact.json 元数据文件(默认 true) */
147
+ emitMetadata?: boolean;
148
+ }
149
+ /**
150
+ * Bun 独立编译器输出结果。
151
+ */
152
+ export interface BunCompilerResult {
153
+ /** 所属 Package ID */
154
+ packageId?: string;
155
+ /** 版本号 */
156
+ version?: string;
157
+ /** 编译目标架构 */
158
+ target: string;
159
+ /** 独立二进制文件绝对路径 */
160
+ executablePath: string;
161
+ /** 独立二进制文件字节大小 */
162
+ sizeBytes: number;
163
+ /** 独立二进制文件的 SHA-256 校验和 */
164
+ sha256: string;
165
+ /** 生成的元数据文件绝对路径(若生成) */
166
+ metadataPath?: string;
167
+ /** 编译是否开启混淆 */
168
+ minify: boolean;
169
+ /** 编译是否启用字节码 */
170
+ bytecode: boolean;
171
+ /** 编译耗时(毫秒) */
172
+ durationMs: number;
173
+ /** 编译完成时间戳 */
174
+ compiledAt: string;
175
+ }
176
+ /**
177
+ * 归档压缩格式。
178
+ */
179
+ export type ArchiveFormat = "zip" | "tar.gz";
180
+ /**
181
+ * Skill 导出选项。
182
+ */
183
+ export interface SkillExporterOptions {
184
+ /** 源码项目根目录绝对路径 */
185
+ projectRoot: string;
186
+ /** 导出模式:source 源码模式 或 standalone 独立二进制模式(默认 source) */
187
+ mode?: "source" | "standalone";
188
+ /** 是否开启独立二进制模式(简写选项) */
189
+ standalone?: boolean;
190
+ /** 二进制编译目标架构(仅在独立二进制模式生效,默认 host) */
191
+ target?: CompileTarget | string;
192
+ /** 导出产物目标目录(默认输出至 dist 目录) */
193
+ outDir?: string;
194
+ /** 是否执行归档压缩,亦可直接指定归档格式 */
195
+ archive?: boolean | ArchiveFormat;
196
+ /** 归档压缩格式(zip 或 tar.gz) */
197
+ archiveFormat?: ArchiveFormat;
198
+ /** 挑选的 Playbook 列表(用于 Playbook 驱动的依赖闭包裁剪) */
199
+ playbooks?: string[];
200
+ /** 挑选的 Action 列表(用于 Action 驱动的依赖闭包裁剪) */
201
+ actions?: string[];
202
+ /** 独立二进制模式下是否开启代码混淆(默认 true) */
203
+ minify?: boolean;
204
+ /** 独立二进制模式下是否编译为字节码(默认 true) */
205
+ bytecode?: boolean;
206
+ /** 预置的项目配置(可选) */
207
+ config?: ProjectConfig;
208
+ /** 预置的声明式清单(可选) */
209
+ manifest?: ActionDockManifest;
210
+ /** 是否跳过生成 SKILL.md(多包聚合时避免子包重复生成,对外保持唯一定义) */
211
+ skipSkillMd?: boolean;
212
+ /** 显式指定的已有 SKILL.md 文件路径(若提供或存在则直接使用,不再动态生成) */
213
+ skillMdPath?: string;
214
+ }
215
+ /**
216
+ * Skill 导出完成结果。
217
+ */
218
+ export interface SkillExportResult {
219
+ /** 所属 Package ID */
220
+ packageId: string;
221
+ /** 项目版本号 */
222
+ version: string;
223
+ /** 导出模式 */
224
+ mode: "source" | "standalone";
225
+ /** 目标平台架构 */
226
+ target: string;
227
+ /** 生成的 Skill 目录绝对路径 */
228
+ skillDir: string;
229
+ /** 生成的归档文件绝对路径(若开启压缩) */
230
+ archivePath?: string;
231
+ /** 导出的 Action 数量 */
232
+ actionsCount: number;
233
+ /** 导出的 Playbook 数量 */
234
+ playbooksCount: number;
235
+ /** 导出的 Action ID 列表 */
236
+ actions: string[];
237
+ /** 导出的 Playbook ID 列表 */
238
+ playbooks: string[];
239
+ /** 导出的文件相对路径清单 */
240
+ files: string[];
241
+ /** 若复用了已有的 SKILL.md 文件,返回该文件的绝对路径 */
242
+ usedExistingSkillMd?: string;
243
+ }
244
+ /**
245
+ * 批量 Skill 导出选项。
246
+ */
247
+ export interface BatchSkillExportOptions extends Omit<SkillExporterOptions, "projectRoot"> {
248
+ /** 待导出的项目根目录绝对路径列表 */
249
+ projectRoots: string[];
250
+ }
251
+ /**
252
+ * 批量 Skill 导出结果。
253
+ */
254
+ export interface BatchSkillExportResult {
255
+ /** 各包独立导出结果列表 */
256
+ results: SkillExportResult[];
257
+ /** 批量导出根目录 */
258
+ outDir: string;
259
+ /** 导出的总 Action 数量 */
260
+ totalActions: number;
261
+ /** 导出的总 Playbook 数量 */
262
+ totalPlaybooks: number;
263
+ }
264
+ /**
265
+ * 复合 Skill 套件导出选项。
266
+ */
267
+ export interface CompositeSkillExportOptions {
268
+ /** 复合技能套件名称 */
269
+ bundleName: string;
270
+ /** 参与聚合的项目根目录绝对路径列表 */
271
+ projectRoots: string[];
272
+ /** 复合套件输出目标目录(默认输出至 dist 目录) */
273
+ outDir?: string;
274
+ /** 复合技能描述信息 */
275
+ description?: string;
276
+ /** 是否执行归档压缩,亦可直接指定归档格式 */
277
+ archive?: boolean | ArchiveFormat;
278
+ /** 归档压缩格式(zip 或 tar.gz) */
279
+ archiveFormat?: ArchiveFormat;
280
+ /** 工作区根目录(用于检索工作区现有的 SKILL.md 等资产,默认基于 cwd 与项目目录推导) */
281
+ workspaceRoot?: string;
282
+ /** 显式指定的已有 SKILL.md 文件路径(若存在则直接使用,不再动态生成) */
283
+ skillMdPath?: string;
284
+ }
285
+ /**
286
+ * 复合 Skill 套件导出结果。
287
+ */
288
+ export interface CompositeSkillExportResult {
289
+ /** 复合技能套件名称 */
290
+ bundleName: string;
291
+ /** 生成的复合 Skill 目录绝对路径 */
292
+ skillDir: string;
293
+ /** 生成的归档文件绝对路径(若开启压缩) */
294
+ archivePath?: string;
295
+ /** 包含的 Package 数量 */
296
+ packagesCount: number;
297
+ /** 包含的总 Action 数量 */
298
+ actionsCount: number;
299
+ /** 包含的总 Playbook 数量 */
300
+ playbooksCount: number;
301
+ /** 各包的摘要明细 */
302
+ packages: Array<{
303
+ packageId: string;
304
+ actions: string[];
305
+ playbooks: string[];
306
+ }>;
307
+ /** 生成的文件清单 */
308
+ files: string[];
309
+ /** 若复用了已有的 SKILL.md 文件,返回该文件的绝对路径 */
310
+ usedExistingSkillMd?: string;
311
+ }
package/package.json CHANGED
@@ -1,40 +1,39 @@
1
1
  {
2
2
  "name": "@actiondock/builder",
3
- "version": "2.0.11-beta.0",
3
+ "version": "2.0.12",
4
4
  "description": "ActionDock Build Planner, Bun standalone compiler, and Skill exporter",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
7
- "module": "./src/index.ts",
8
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
- "import": "./src/index.ts",
12
- "types": "./src/index.ts"
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
13
14
  }
14
15
  },
15
16
  "files": [
16
- "src",
17
+ "dist",
17
18
  "README.md"
18
19
  ],
19
20
  "engines": {
20
- "node": ">=22.12.0"
21
+ "node": ">=22.13.0"
21
22
  },
22
23
  "publishConfig": {
23
24
  "access": "public",
24
25
  "registry": "https://registry.npmjs.org/"
25
26
  },
26
27
  "scripts": {
28
+ "build": "bun run ../../scripts/build.ts",
27
29
  "test": "bun test"
28
30
  },
29
31
  "dependencies": {
30
- "@actiondock/core": "2.0.11-beta.0",
31
- "@actiondock/sdk": "2.0.11-beta.0",
32
+ "@actiondock/core": "^2.0.12",
33
+ "@actiondock/sdk": "^2.0.12",
34
+ "typescript": "^5.7.0",
32
35
  "yaml": "^2.7.0"
33
36
  },
34
- "devDependencies": {
35
- "@types/bun": "latest",
36
- "typescript": "^5.7.0"
37
- },
38
37
  "keywords": [
39
38
  "actiondock",
40
39
  "builder",
package/src/archive.ts DELETED
@@ -1,248 +0,0 @@
1
- import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { deflateRawSync, gzipSync } from "node:zlib";
3
- import { basename, dirname, join, relative, sep } from "node:path";
4
-
5
- /**
6
- * 纯 Node 实现的归档压缩模块。
7
- *
8
- * zip 与 tar.gz 均在进程内完成打包,不依赖宿主机的 zip / tar 命令行工具
9
- * (Windows 与最小化 Linux 环境普遍缺少 zip 命令)。
10
- * 压缩能力复用 node:zlib 内置的 deflateRaw(zip 条目)与 gzip(tar.gz 容器)。
11
- */
12
-
13
- /** 目录内相对路径条目(目录在前、同级按名称排序,保证归档内容顺序稳定) */
14
- interface ArchiveEntry {
15
- /** 相对根目录的 POSIX 风格路径(目录以 / 结尾仅出现在 zip 条目名层面) */
16
- relPath: string;
17
- /** 是否为目录 */
18
- isDir: boolean;
19
- }
20
-
21
- /** 递归收集目录内全部条目 */
22
- function collectEntries(dir: string, baseDir = dir): ArchiveEntry[] {
23
- const entries: ArchiveEntry[] = [];
24
- for (const name of readdirSync(dir).sort()) {
25
- const fullPath = join(dir, name);
26
- const stat = statSync(fullPath);
27
- if (stat.isDirectory()) {
28
- entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: true });
29
- entries.push(...collectEntries(fullPath, baseDir));
30
- } else if (stat.isFile()) {
31
- entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: false });
32
- }
33
- }
34
- return entries;
35
- }
36
-
37
- /* ------------------------------------------------------------------ */
38
- /* zip 格式写入 */
39
- /* ------------------------------------------------------------------ */
40
-
41
- /** CRC32 查表(IEEE 802.3 多项式) */
42
- const CRC32_TABLE: Uint32Array = (() => {
43
- const table = new Uint32Array(256);
44
- for (let n = 0; n < 256; n++) {
45
- let c = n;
46
- for (let k = 0; k < 8; k++) {
47
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
48
- }
49
- table[n] = c >>> 0;
50
- }
51
- return table;
52
- })();
53
-
54
- function crc32(buf: Buffer): number {
55
- let crc = 0xffffffff;
56
- for (let i = 0; i < buf.length; i++) {
57
- crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
58
- }
59
- return (crc ^ 0xffffffff) >>> 0;
60
- }
61
-
62
- /** Unix 毫秒时间戳转 DOS 时间格式(date/time 各 16 位) */
63
- function dosDateTime(ms: number): { date: number; time: number } {
64
- const d = new Date(ms);
65
- const year = Math.max(d.getFullYear(), 1980);
66
- const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
67
- const date = ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
68
- return { date, time };
69
- }
70
-
71
- /** central directory 记录(写入 EOCD 前聚合) */
72
- interface ZipCentralRecord {
73
- header: Buffer;
74
- localOffset: number;
75
- }
76
-
77
- /**
78
- * 将目录打包为标准 zip 归档(PKZIP APPNOTE 结构)。
79
- *
80
- * @param dir 待打包目录(目录名即归档内根文件夹名)
81
- * @param outPath 输出 zip 文件路径
82
- */
83
- export function createZipArchive(dir: string, outPath: string): void {
84
- const rootName = basename(dir);
85
- const entries = collectEntries(dir);
86
- const chunks: Buffer[] = [];
87
- const centralRecords: ZipCentralRecord[] = [];
88
- let offset = 0;
89
-
90
- for (const entry of entries) {
91
- // 归档内路径带根目录前缀;目录条目名以 / 结尾
92
- const name = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
93
- const nameBuf = Buffer.from(name, "utf8");
94
- const stat = statSync(join(dir, entry.relPath));
95
- const { date, time } = dosDateTime(stat.mtimeMs);
96
- const content = entry.isDir ? Buffer.alloc(0) : readFileSync(join(dir, entry.relPath));
97
-
98
- let method = 0;
99
- let payload = content;
100
- if (!entry.isDir && content.length > 0) {
101
- const deflated = deflateRawSync(content, { level: 9 });
102
- if (deflated.length < content.length) {
103
- method = 8;
104
- payload = deflated;
105
- }
106
- }
107
-
108
- const crc = crc32(content);
109
-
110
- // Local File Header(30 字节 + 文件名)
111
- const local = Buffer.alloc(30);
112
- local.writeUInt32LE(0x04034b50, 0);
113
- local.writeUInt16LE(20, 4);
114
- local.writeUInt16LE(0x0800, 6); // UTF-8 文件名标志
115
- local.writeUInt16LE(method, 8);
116
- local.writeUInt16LE(time, 10);
117
- local.writeUInt16LE(date, 12);
118
- local.writeUInt32LE(crc, 14);
119
- local.writeUInt32LE(payload.length, 18);
120
- local.writeUInt32LE(content.length, 22);
121
- local.writeUInt16LE(nameBuf.length, 26);
122
- local.writeUInt16LE(0, 28);
123
- chunks.push(local, nameBuf, payload);
124
-
125
- // Central Directory Header(46 字节 + 文件名)
126
- const central = Buffer.alloc(46);
127
- central.writeUInt32LE(0x02014b50, 0);
128
- central.writeUInt16LE(20, 4); // version made by
129
- central.writeUInt16LE(20, 6); // version needed
130
- central.writeUInt16LE(0x0800, 8);
131
- central.writeUInt16LE(method, 10);
132
- central.writeUInt16LE(time, 12);
133
- central.writeUInt16LE(date, 14);
134
- central.writeUInt32LE(crc, 16);
135
- central.writeUInt32LE(payload.length, 20);
136
- central.writeUInt32LE(content.length, 24);
137
- central.writeUInt16LE(nameBuf.length, 28);
138
- central.writeUInt16LE(0, 30); // extra len
139
- central.writeUInt16LE(0, 32); // comment len
140
- central.writeUInt16LE(0, 34); // disk start
141
- central.writeUInt16LE(0, 36); // internal attrs
142
- // 外部属性:Unix 权限左移 16 位,目录附加 MS-DOS 目录位
143
- const extAttrs = ((entry.isDir ? 0o40755 : 0o100644) << 16) | (entry.isDir ? 0x10 : 0);
144
- central.writeUInt32LE(extAttrs >>> 0, 38);
145
- central.writeUInt32LE(offset, 42);
146
- centralRecords.push({ header: Buffer.concat([central, nameBuf]), localOffset: offset });
147
-
148
- offset += local.length + nameBuf.length + payload.length;
149
- }
150
-
151
- const centralDirOffset = offset;
152
- let centralDirSize = 0;
153
- for (const record of centralRecords) {
154
- chunks.push(record.header);
155
- centralDirSize += record.header.length;
156
- }
157
-
158
- // End of Central Directory Record(22 字节)
159
- const eocd = Buffer.alloc(22);
160
- eocd.writeUInt32LE(0x06054b50, 0);
161
- eocd.writeUInt16LE(0, 4);
162
- eocd.writeUInt16LE(0, 6);
163
- eocd.writeUInt16LE(centralRecords.length, 8);
164
- eocd.writeUInt16LE(centralRecords.length, 10);
165
- eocd.writeUInt32LE(centralDirSize, 12);
166
- eocd.writeUInt32LE(centralDirOffset, 16);
167
- eocd.writeUInt16LE(0, 20); // comment len
168
- chunks.push(eocd);
169
-
170
- writeFileSync(outPath, Buffer.concat(chunks));
171
- }
172
-
173
- /* ------------------------------------------------------------------ */
174
- /* tar.gz 格式写入 */
175
- /* ------------------------------------------------------------------ */
176
-
177
- /**
178
- * 构造单个条目的 USTAR 头块(512 字节)。
179
- * 路径超长时按 USTAR prefix 字段拆分,仍放不下则抛错。
180
- */
181
- function tarHeader(path: string, size: number, mtimeSec: number, isDir: boolean): Buffer {
182
- let name = path;
183
- let prefix = "";
184
- if (Buffer.byteLength(name, "utf8") > 100) {
185
- const split = name.lastIndexOf("/");
186
- if (split <= 0 || split > 155 || name.length - split - 1 > 100) {
187
- throw new Error(`归档路径超出 USTAR 字段容量: ${path}`);
188
- }
189
- prefix = name.slice(0, split);
190
- name = name.slice(split + 1);
191
- }
192
-
193
- const buf = Buffer.alloc(512);
194
- buf.write(name, 0, 100, "utf8");
195
- buf.write(isDir ? "0000755" : "0000644", 100, 8, "ascii"); // mode
196
- buf.write("0000000", 108, 8, "ascii"); // uid
197
- buf.write("0000000", 116, 8, "ascii"); // gid
198
- buf.write(size.toString(8).padStart(11, "0"), 124, 12, "ascii"); // size
199
- buf.write(Math.floor(mtimeSec).toString(8).padStart(11, "0"), 136, 12, "ascii"); // mtime
200
- buf.fill(" ", 148, 156); // chksum 占位(计算时视为空格)
201
- buf.write(isDir ? "5" : "0", 156, 1, "ascii"); // typeflag
202
- buf.write("ustar\0", 257, 6, "ascii"); // magic
203
- buf.write("00", 263, 2, "ascii"); // version
204
- if (prefix) buf.write(prefix, 345, 155, "utf8");
205
-
206
- // 校验和:整块字节的无符号累加和,写作 6 位八进制 + NUL + 空格
207
- let sum = 0;
208
- for (let i = 0; i < 512; i++) sum += buf[i];
209
- buf.write(sum.toString(8).padStart(6, "0"), 148, 7, "ascii");
210
- buf.write("\0", 154, 1, "ascii");
211
- buf.write(" ", 155, 1, "ascii");
212
- return buf;
213
- }
214
-
215
- /** 将字节数补齐到 512 的整数倍 */
216
- function pad512(length: number): Buffer {
217
- const rem = length % 512;
218
- return rem === 0 ? Buffer.alloc(0) : Buffer.alloc(512 - rem);
219
- }
220
-
221
- /**
222
- * 将目录打包为标准 tar.gz 归档(USTAR + gzip)。
223
- *
224
- * @param dir 待打包目录(目录名即归档内根文件夹名)
225
- * @param outPath 输出 tar.gz 文件路径
226
- */
227
- export function createTarGzArchive(dir: string, outPath: string): void {
228
- const rootName = basename(dir);
229
- const entries = collectEntries(dir);
230
- const chunks: Buffer[] = [];
231
-
232
- for (const entry of entries) {
233
- const path = `${rootName}/${entry.relPath}`;
234
- const fullPath = join(dir, entry.relPath);
235
- const stat = statSync(fullPath);
236
- const content = entry.isDir ? Buffer.alloc(0) : readFileSync(fullPath);
237
-
238
- chunks.push(tarHeader(path, content.length, Math.floor(stat.mtimeMs / 1000), entry.isDir));
239
- if (!entry.isDir) {
240
- chunks.push(content, pad512(content.length));
241
- }
242
- }
243
-
244
- // 归档结尾:两个全零块
245
- chunks.push(Buffer.alloc(1024));
246
-
247
- writeFileSync(outPath, gzipSync(Buffer.concat(chunks), { level: 9 }));
248
- }