tirtc-device-builder 0.2.0 → 0.3.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-device-builder",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Codex workflows for building and validating TiRTC device firmware across supported chip platforms.",
5
5
  "author": {
6
6
  "name": "TangeAI",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  This project follows Semantic Versioning.
4
4
 
5
+ ## 0.3.0
6
+
7
+ - Add reproducible packaging for a minimal, checksummed ESP32-S3 Device Kit release asset.
8
+ - Download, verify, cache, and reuse the pinned Device Kit instead of cloning the full ThingConnect source repository during automatic setup.
9
+ - Add a read-only `setup esp32` check that reports the smallest next action.
10
+ - Add `setup esp32 --install` for resumable user-space installation of the Codex Skill, versioned ESP32 Device Kit, ESP-IDF 5.5.4, and ESP32-S3 tools.
11
+ - Reuse valid existing workspaces and ESP-IDF installations without overwriting them.
12
+ - Keep system package installation and persistent shell-profile changes outside automatic setup.
13
+ - Save a path-only managed configuration and environment helper for later Skill runs.
14
+
5
15
  ## 0.2.0
6
16
 
7
17
  - Publish the repository as the `tirtc-device-builder` npm package.
package/README.md CHANGED
@@ -12,11 +12,101 @@ TiRTC Device Builder 是一个面向设备开发者的 Codex Plugin 仓库。它
12
12
 
13
13
  ## 能力边界
14
14
 
15
- ESP32 Skill 使用公开的 [ThingConnect 示例仓库](https://github.com/tangeai/tirtc-server-example) 作为协议、模板、生成器和 TiRTC ESP32 SDK 的事实源。这个仓库不复制服务端源码、预编译 TiRTC SDK、设备凭证或媒体样本。
15
+ ESP32 开发资源以独立、带版本和 SHA-256 的 Device Kit 发布。Kit 的事实源是公开的 [ThingConnect 示例仓库](https://github.com/tangeai/tirtc-server-example),但普通开发者不需要克隆该服务端仓库。Device Kit 不包含设备凭证、板卡 BSP 或用户媒体。
16
+
17
+ ## 维护者:发布最小 ESP32 资源包
18
+
19
+ 最小资源包只包含工程生成器、`esp32-h5-ai` 模板、`platform_client`、`runtime_config`、`wifi_manager`、TiRTC ESP32-S3 SDK 2.3.0 和相关设备协议文档。打包命令会检查白名单资源、记录源码提交、生成确定性压缩包和 SHA-256:
20
+
21
+ ```bash
22
+ cd /home/workspace/tirtc-device-builder
23
+
24
+ npm run pack:esp32-kit -- \
25
+ --source /home/workspace/tirtc-server-example/thing-connect \
26
+ --kit-version 1.0.0
27
+ ```
28
+
29
+ 输出文件位于 `dist/`:
30
+
31
+ ```text
32
+ tirtc-esp32s3-kit-1.0.0.tar.gz
33
+ tirtc-esp32s3-kit-1.0.0.tar.gz.sha256
34
+ ```
35
+
36
+ 从 `dist/` 目录校验附件:
37
+
38
+ ```bash
39
+ cd /home/workspace/tirtc-device-builder/dist
40
+ sha256sum -c tirtc-esp32s3-kit-1.0.0.tar.gz.sha256
41
+ ```
42
+
43
+ 发布前使用独立标签,并把两个文件作为 GitHub Release 附件上传:
44
+
45
+ ```bash
46
+ git tag -a kit-esp32s3-v1.0.0 -m "TiRTC ESP32-S3 Device Kit 1.0.0"
47
+ git push origin kit-esp32s3-v1.0.0
48
+
49
+ gh release create kit-esp32s3-v1.0.0 \
50
+ dist/tirtc-esp32s3-kit-1.0.0.tar.gz \
51
+ dist/tirtc-esp32s3-kit-1.0.0.tar.gz.sha256 \
52
+ --repo tangeai/tirtc-device-builder \
53
+ --verify-tag \
54
+ --latest=false \
55
+ --title "TiRTC ESP32-S3 Device Kit 1.0.0" \
56
+ --notes "ESP-IDF 5.5.x;TiRTC SDK 2.3.0;包含 H5/AI 工程生成资源。"
57
+ ```
16
58
 
17
59
  模板工程提供配网、绑定、MQTT、TiRTC、H5 和 AI 会话骨架。具体开发板仍需接入摄像头、H.264 编码器、麦克风、扬声器、Codec、I2S 和按键。工程编译成功不等于 Web 已经出图或 AI 对讲已通过实机验收。
18
60
 
19
- ## 新人从零开始:一次跑通 ESP32-S3 H5/AI
61
+ ## 推荐:一键检查和安装
62
+
63
+ 新人先执行一条只读命令:
64
+
65
+ ```bash
66
+ npx tirtc-device-builder@latest setup esp32
67
+ ```
68
+
69
+ 环境完整时它直接运行 Doctor 并输出 `SETUP: READY`。存在缺失项时,它会显示具体状态和下一条命令:
70
+
71
+ ```bash
72
+ npx tirtc-device-builder@latest setup esp32 --install
73
+ ```
74
+
75
+ `--install` 自动完成:
76
+
77
+ - 安装 `tirtc-esp32-builder` 到 Codex Skill 目录;
78
+ - 下载并校验 ESP32 Device Kit 1.0.0,缓存后供多个工程复用;
79
+ - 复用已激活的 ESP-IDF 5.5.x,缺失时安装固定版本 ESP-IDF 5.5.4;
80
+ - 安装 Espressif 管理的 ESP32-S3 工具链;
81
+ - 生成只包含路径的 `config.json` 和 `env.sh`;
82
+ - 自动激活安装环境并重新运行 Doctor。
83
+
84
+ 默认托管目录是 `~/.tirtc-device-builder`。自动安装不执行 `sudo`、不修改 `.bashrc` 或 `.zshrc`、不覆盖已有但内容不完整的目录。缺少 Git、Python 或其他系统依赖时,它会停止并给出可复制的系统安装命令;修复后重复同一条 `--install` 命令即可从断点继续。
85
+
86
+ 需要自定义位置时:
87
+
88
+ ```bash
89
+ npx tirtc-device-builder@latest setup esp32 --install \
90
+ --root /absolute/path/tirtc-dev
91
+ ```
92
+
93
+ 安装完成后启动新的 Codex 会话,只需输入板卡和目标:
94
+
95
+ ```text
96
+ $tirtc-esp32-builder
97
+
98
+ 开发板:<厂商> <完整型号> <PCB 版本>
99
+ 板卡资料:/absolute/path/board-materials
100
+ 输出工程:/absolute/path/my-esp32-device
101
+ 目标:H5 实时音视频、H5 对讲、AI 双向对讲
102
+
103
+ 使用一键安装产生的托管环境。先分析资料并生成 Hardware IR,
104
+ 达到 READY_TO_PORT 后生成和编译;本轮不烧录。
105
+ ```
106
+
107
+ 到这里,新人只需要“执行 `setup --install` → 重启 Codex → 输入板卡需求”。下面保留完整流程,供自定义目录、手动安装和故障排查使用。
108
+
109
+ ## 完整手动流程和排障
20
110
 
21
111
  推荐先跑通“安装 → 环境 → 板卡分析 → 生成 → 编译”,确认报告无误后再单独授权烧录和实机验收。整个过程使用同一份板卡事实,避免在代码生成阶段猜测引脚、器件或媒体能力。
22
112
 
@@ -45,7 +135,7 @@ TIRTC_PORTING_REPORT.md
45
135
  | ESP-IDF | 5.5.x |
46
136
  | TiRTC SDK | `espressif-esp32s3/2.3.0` |
47
137
  | 业务 | H5 实时音视频、H5 对讲、AI 双向语音 |
48
- | 工程生成 | 需要一次本地 ThingConnect 工作区 |
138
+ | 工程生成 | 使用本地缓存的 ESP32 Device Kit 1.0.0 |
49
139
  | 烧录 | 必须明确给出串口并授权 |
50
140
 
51
141
  “板上有摄像头”不等于可以输出 H5 视频。H5 视频还需要可用的 H.264 Annex-B 编码输出、SPS/PPS、IDR 和关键帧请求控制;对讲还需要完整的麦克风采集、G.711 A-law 8 kHz 编码、下行解码、I2S/Codec/功放和扬声器路径。
@@ -58,16 +148,17 @@ TIRTC_PORTING_REPORT.md
58
148
 
59
149
  ```bash
60
150
  export TIRTC_WORKSPACE=/home/your-user/workspace
61
- export TIRTC_THING_CONNECT_ROOT="$TIRTC_WORKSPACE/tirtc-server-example/thing-connect"
62
151
  export TIRTC_BOARD_DOCS="$TIRTC_WORKSPACE/board-materials"
63
152
  export TIRTC_PROJECT_DIR="$TIRTC_WORKSPACE/my-esp32-device"
153
+
154
+ source ~/.tirtc-device-builder/env.sh
64
155
  ```
65
156
 
66
157
  逐项确认:
67
158
 
68
159
  ```bash
69
160
  printf 'workspace: %s\n' "$TIRTC_WORKSPACE"
70
- printf 'ThingConnect: %s\n' "$TIRTC_THING_CONNECT_ROOT"
161
+ printf 'ESP32 Device Kit: %s\n' "$TIRTC_THING_CONNECT_ROOT"
71
162
  printf 'board docs: %s\n' "$TIRTC_BOARD_DOCS"
72
163
  printf 'output project: %s\n' "$TIRTC_PROJECT_DIR"
73
164
  ```
@@ -130,14 +221,14 @@ npm config set registry https://registry.npmjs.org/
130
221
 
131
222
  ```bash
132
223
  npm view tirtc-device-builder version
133
- npx tirtc-device-builder@0.2.0 list
134
- npx tirtc-device-builder@0.2.0 install esp32
224
+ npx tirtc-device-builder@latest list
225
+ npx tirtc-device-builder@latest install esp32
135
226
  ```
136
227
 
137
228
  成功时最后会看到类似输出:
138
229
 
139
230
  ```text
140
- Installed tirtc-esp32-builder 0.2.0 to /home/.../.codex/skills/tirtc-esp32-builder
231
+ Installed tirtc-esp32-builder <version> to /home/.../.codex/skills/tirtc-esp32-builder
141
232
  Start a new Codex session, then invoke $tirtc-esp32-builder.
142
233
  ```
143
234
 
@@ -146,13 +237,13 @@ Start a new Codex session, then invoke $tirtc-esp32-builder.
146
237
  如果目标目录已经存在,安装器会保护已有内容并退出。先确认目录中的本地修改是否需要保留;只有确定要替换时才执行:
147
238
 
148
239
  ```bash
149
- npx tirtc-device-builder@0.2.0 install esp32 --force
240
+ npx tirtc-device-builder@latest install esp32 --force
150
241
  ```
151
242
 
152
243
  自定义 Skill 根目录时使用绝对路径:
153
244
 
154
245
  ```bash
155
- npx tirtc-device-builder@0.2.0 install esp32 \
246
+ npx tirtc-device-builder@latest install esp32 \
156
247
  --skills-dir /absolute/path/to/skills
157
248
  ```
158
249
 
@@ -162,7 +253,7 @@ npx tirtc-device-builder@0.2.0 install esp32 \
162
253
  $skill-installer
163
254
 
164
255
  安装:
165
- https://github.com/tangeai/tirtc-device-builder/tree/v0.2.0/skills/tirtc-esp32-builder
256
+ https://github.com/tangeai/tirtc-device-builder/tree/v0.3.0/skills/tirtc-esp32-builder
166
257
  ```
167
258
 
168
259
  Linux/macOS 也可以执行:
@@ -170,27 +261,33 @@ Linux/macOS 也可以执行:
170
261
  ```bash
171
262
  python3 ~/.codex/skills/.system/skill-installer/scripts/install-skill-from-github.py \
172
263
  --repo tangeai/tirtc-device-builder \
173
- --ref v0.2.0 \
264
+ --ref v0.3.0 \
174
265
  --path skills/tirtc-esp32-builder
175
266
  ```
176
267
 
177
268
  仓库根目录还包含 `.codex-plugin/plugin.json`,可作为 skills-only Plugin 使用。直接从 npm 或 GitHub 安装不依赖 Plugin Directory 审核。
178
269
 
179
- ### 第 4 步:准备 ThingConnect 工作区
270
+ ### 第 4 步:准备 ESP32 Device Kit
180
271
 
181
- 当前 Skill 不复制 ThingConnect 服务端源码和 TiRTC 预编译库。生成新工程时,以下内容来自公开的 ThingConnect 仓库:
272
+ Device Kit 包含生成新工程需要的最小资源:
182
273
 
183
274
  - ESP32-S3 H5/AI 工程生成器;
184
275
  - 起步模板和平台公共组件;
185
276
  - TiRTC ESP32-S3 SDK 头文件、静态库和构建契约;
186
277
  - H5、AI、设备接入和会话协议文档。
187
278
 
188
- 只需准备一次,后续多个工程可以共用同一工作区:
279
+ 一键安装会下载固定 Release、校验 SHA-256,并缓存到 `~/.tirtc-device-builder/kits/esp32s3/1.0.0/`。不需要克隆 `tirtc-server-example`:
280
+
281
+ ```bash
282
+ npx tirtc-device-builder@latest setup esp32 --install
283
+ source ~/.tirtc-device-builder/env.sh
284
+ ```
285
+
286
+ 维护者刚生成 Release、附件还没有上传时,可以使用本地包完成同样的安装验证:
189
287
 
190
288
  ```bash
191
- mkdir -p "$TIRTC_WORKSPACE"
192
- git clone https://github.com/tangeai/tirtc-server-example.git \
193
- "$TIRTC_WORKSPACE/tirtc-server-example"
289
+ npx tirtc-device-builder@latest setup esp32 --install \
290
+ --kit-archive /absolute/path/tirtc-esp32s3-kit-1.0.0.tar.gz
194
291
  ```
195
292
 
196
293
  验证关键文件:
@@ -202,13 +299,13 @@ ls -l "$TIRTC_THING_CONNECT_ROOT/device-sim/sdk/espressif-esp32s3/2.3.0/lib/libT
202
299
  ls -l "$TIRTC_THING_CONNECT_ROOT/device-sim/sdk/espressif-esp32s3/2.3.0/manifest/build-contract.env"
203
300
  ```
204
301
 
205
- 四条命令都应显示真实文件。记录当前源码版本,便于报告和复现:
302
+ 四条命令都应显示真实文件。资源来源版本记录在 Kit 清单中:
206
303
 
207
304
  ```bash
208
- git -C "$TIRTC_WORKSPACE/tirtc-server-example" rev-parse HEAD
305
+ python3 -m json.tool "$TIRTC_THING_CONNECT_ROOT/manifest.json"
209
306
  ```
210
307
 
211
- 生成器会把必要模块和 TiRTC SDK 复制到新工程的 `third_party/tirtc/`,所以已生成的工程可以移动并独立编译。安装 Skill、列出平台、分析本地板卡资料,以及诊断一个已经自带 SDK 的生成工程,不要求每次重新拉取 ThingConnect。
308
+ 生成器会把必要模块和 TiRTC SDK 复制到新工程的 `third_party/tirtc/`,所以已生成的工程可以移动并独立编译。一个已校验的 Kit 缓存可以供多个工程使用。
212
309
 
213
310
  ### 第 5 步:检查或安装 ESP-IDF 5.5.x
214
311
 
@@ -259,7 +356,7 @@ command -v xtensa-esp32s3-elf-gcc
259
356
  不安装 Skill 也可以通过 npm CLI 调用同一个检查脚本:
260
357
 
261
358
  ```bash
262
- npx tirtc-device-builder@0.2.0 doctor esp32 \
359
+ npx tirtc-device-builder@latest doctor esp32 \
263
360
  --expected-idf 5.5 \
264
361
  --target esp32s3 \
265
362
  --thing-connect-root "$TIRTC_THING_CONNECT_ROOT" \
@@ -283,7 +380,7 @@ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/doctor.py \
283
380
  | Python、Git | `PASS` |
284
381
  | `idf.py` | `PASS`,版本为 5.5.x |
285
382
  | target compiler | `PASS`,找到 ESP32-S3 编译器 |
286
- | ThingConnect workspace | `PASS` |
383
+ | ESP32 Device Kit | `PASS` |
287
384
  | TiRTC SDK | `PASS` |
288
385
  | serial discovery | 还未接板时允许 `WARN` |
289
386
  | OVERALL | `PASS` |
@@ -294,7 +391,7 @@ python3 ~/.codex/skills/tirtc-esp32-builder/scripts/doctor.py \
294
391
 
295
392
  - `idf.py not found`:当前终端没有激活 ESP-IDF,先执行 `source <idf-dir>/export.sh`。
296
393
  - `target compiler ... not found`:ESP-IDF 环境未激活,或安装时没有包含 `esp32s3`。
297
- - `ThingConnect workspace ... not found`:传入的路径必须是仓库根目录或其 `thing-connect/` 子目录,并且其中存在生成器。
394
+ - `ESP32 Device Kit ... not found`:先执行一键安装,或传入 Kit 根目录;兼容路径也可以是完整仓库根目录或其 `thing-connect/` 子目录。
298
395
  - `TiRTC SDK ... missing`:检查仓库是否拉取完整,以及 SDK 的头文件、静态库和 `manifest/build-contract.env` 是否存在。
299
396
  - `serial discovery: no serial device detected`:生成和编译阶段可以继续;烧录前必须解决。
300
397
 
@@ -324,7 +421,7 @@ FAIL TiRTC build contract [required]: no sdkconfig or sdkconfig.defaults in ...
324
421
  - BOM:/absolute/path/board-materials/<file>
325
422
  - BSP/厂商示例:/absolute/path/vendor-bsp
326
423
  - 摄像头/Codec/功放数据手册:/absolute/path/board-materials/<files>
327
- - ThingConnect:/absolute/path/tirtc-server-example/thing-connect
424
+ - Device Kit:使用 `setup esp32 --install` 生成的托管环境
328
425
 
329
426
  目标:
330
427
  - H5 实时视频和音频
@@ -554,7 +651,7 @@ AI 验收:
554
651
  - [ ] 板卡厂商、完整型号、模组、PCB 版本一致。
555
652
  - [ ] 原理图/BOM/BSP/数据手册路径均为绝对路径且可读。
556
653
  - [ ] Node.js 版本不低于 18,Skill 已安装并重启 Codex。
557
- - [ ] ThingConnect 生成器、TiRTC 头文件、静态库和构建契约存在。
654
+ - [ ] ESP32 Device Kit 的生成器、TiRTC 头文件、静态库和构建契约存在。
558
655
  - [ ] 当前终端已激活 ESP-IDF 5.5.x 和 ESP32-S3 工具链。
559
656
  - [ ] 生成前 Doctor 的 `OVERALL` 为 `PASS`。
560
657
  - [ ] 所有请求能力均为 `READY_TO_PORT` 或 `HIL_VERIFIED`。
@@ -0,0 +1,8 @@
1
+ export const ESP32_KIT = Object.freeze({
2
+ archiveName: "tirtc-esp32s3-kit-1.0.0.tar.gz",
3
+ archiveRoot: "tirtc-esp32s3-kit-1.0.0",
4
+ releaseTag: "kit-esp32s3-v1.0.0",
5
+ sha256: "d2333878ae499a349b125cd81db0a64aef62bfae8fd7c0898c8eaa5ff2a49b65",
6
+ url: "https://github.com/tangeai/tirtc-device-builder/releases/download/kit-esp32s3-v1.0.0/tirtc-esp32s3-kit-1.0.0.tar.gz",
7
+ version: "1.0.0",
8
+ });
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import {
6
+ existsSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ mkdtempSync,
10
+ readFileSync,
11
+ readdirSync,
12
+ renameSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { basename, dirname, join, relative, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { ESP32_KIT } from "./esp32-kit-metadata.js";
20
+
21
+ const REQUIRED_FILES = [
22
+ "manifest.json",
23
+ "device-sim/scripts/create_esp32_project.py",
24
+ "device-sim/templates/esp32-h5-ai/CMakeLists.txt",
25
+ "device-sim/sdk/espressif-esp32s3/2.3.0/include/tirtc/tiRTC.h",
26
+ "device-sim/sdk/espressif-esp32s3/2.3.0/lib/libTiRTC.a",
27
+ "device-sim/sdk/espressif-esp32s3/2.3.0/manifest/build-contract.env",
28
+ ];
29
+
30
+ function hashFile(path) {
31
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
32
+ }
33
+
34
+ function runTar(args) {
35
+ const result = spawnSync("tar", args, { encoding: "utf8" });
36
+ if (result.error) {
37
+ throw new Error(`could not run tar: ${result.error.message}`);
38
+ }
39
+ if (result.status !== 0) {
40
+ throw new Error(
41
+ `tar exited with status ${result.status}: ${result.stderr.trim()}`,
42
+ );
43
+ }
44
+ return result.stdout;
45
+ }
46
+
47
+ function assertArchivePaths(archive, archiveRoot) {
48
+ const entries = runTar(["-tzf", archive])
49
+ .split(/\r?\n/)
50
+ .filter(Boolean);
51
+ if (entries.length === 0) {
52
+ throw new Error("Device Kit archive is empty");
53
+ }
54
+ for (const entry of entries) {
55
+ if (
56
+ entry.startsWith("/") ||
57
+ entry.split("/").includes("..") ||
58
+ !(entry === archiveRoot || entry.startsWith(`${archiveRoot}/`))
59
+ ) {
60
+ throw new Error(`unsafe Device Kit archive path: ${entry}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ function assertNoLinks(root, current = root) {
66
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
67
+ const path = join(current, entry.name);
68
+ const stat = lstatSync(path);
69
+ if (stat.isSymbolicLink()) {
70
+ throw new Error(`Device Kit contains a symbolic link: ${relative(root, path)}`);
71
+ }
72
+ if (stat.isDirectory()) {
73
+ assertNoLinks(root, path);
74
+ } else if (!stat.isFile()) {
75
+ throw new Error(`Device Kit contains an unsupported entry: ${relative(root, path)}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ function validateExtractedKit(root, metadata) {
81
+ const missing = REQUIRED_FILES.filter((path) => !existsSync(join(root, path)));
82
+ if (missing.length > 0) {
83
+ throw new Error(`Device Kit is incomplete: ${missing.join(", ")}`);
84
+ }
85
+ assertNoLinks(root);
86
+ let manifest;
87
+ try {
88
+ manifest = JSON.parse(readFileSync(join(root, "manifest.json"), "utf8"));
89
+ } catch (error) {
90
+ throw new Error(
91
+ `Device Kit manifest is invalid: ${error instanceof Error ? error.message : String(error)}`,
92
+ );
93
+ }
94
+ if (manifest.kit_version !== metadata.version) {
95
+ throw new Error(
96
+ `Device Kit version mismatch: expected ${metadata.version}, got ${manifest.kit_version}`,
97
+ );
98
+ }
99
+ if (manifest.target !== "esp32s3" || manifest.tirtc_sdk_version !== "2.3.0") {
100
+ throw new Error("Device Kit target or TiRTC SDK version is incompatible");
101
+ }
102
+ if (!manifest.files || typeof manifest.files !== "object") {
103
+ throw new Error("Device Kit manifest has no file checksums");
104
+ }
105
+ for (const [path, expected] of Object.entries(manifest.files)) {
106
+ if (
107
+ typeof expected !== "string" ||
108
+ path.startsWith("/") ||
109
+ path.split("/").includes("..")
110
+ ) {
111
+ throw new Error(`Device Kit manifest contains an unsafe entry: ${path}`);
112
+ }
113
+ const file = join(root, path);
114
+ if (!existsSync(file) || hashFile(file) !== expected) {
115
+ throw new Error(`Device Kit file checksum mismatch: ${path}`);
116
+ }
117
+ }
118
+ }
119
+
120
+ export function installEsp32KitArchive(archive, target, metadata = ESP32_KIT) {
121
+ const resolvedArchive = resolve(archive);
122
+ const resolvedTarget = resolve(target);
123
+ if (!existsSync(resolvedArchive)) {
124
+ throw new Error(`Device Kit archive does not exist: ${resolvedArchive}`);
125
+ }
126
+ const actualHash = hashFile(resolvedArchive);
127
+ if (actualHash !== metadata.sha256) {
128
+ throw new Error(
129
+ `Device Kit SHA-256 mismatch: expected ${metadata.sha256}, got ${actualHash}`,
130
+ );
131
+ }
132
+ if (existsSync(resolvedTarget)) {
133
+ throw new Error(`refusing to overwrite existing Device Kit: ${resolvedTarget}`);
134
+ }
135
+ mkdirSync(dirname(resolvedTarget), { recursive: true });
136
+ const temporary = mkdtempSync(join(dirname(resolvedTarget), ".kit-install-"));
137
+ try {
138
+ assertArchivePaths(resolvedArchive, metadata.archiveRoot);
139
+ runTar(["-xzf", resolvedArchive, "-C", temporary]);
140
+ const extracted = join(temporary, metadata.archiveRoot);
141
+ validateExtractedKit(extracted, metadata);
142
+ renameSync(extracted, resolvedTarget);
143
+ } finally {
144
+ rmSync(temporary, { force: true, recursive: true });
145
+ }
146
+ console.log(`Installed ESP32 Device Kit ${metadata.version} to ${resolvedTarget}`);
147
+ }
148
+
149
+ async function download(url, destination) {
150
+ console.log(`GET ${url}`);
151
+ const response = await fetch(url, { redirect: "follow" });
152
+ if (!response.ok) {
153
+ throw new Error(`Device Kit download failed: HTTP ${response.status}`);
154
+ }
155
+ const content = Buffer.from(await response.arrayBuffer());
156
+ writeFileSync(destination, content, { mode: 0o600 });
157
+ }
158
+
159
+ function parseArgs(args) {
160
+ let archive = null;
161
+ let target = null;
162
+ for (let index = 0; index < args.length; index += 1) {
163
+ const argument = args[index];
164
+ if (argument === "--archive" || argument === "--target") {
165
+ const value = args[index + 1];
166
+ if (!value || value.startsWith("--")) {
167
+ throw new Error(`${argument} requires a path`);
168
+ }
169
+ if (argument === "--archive") {
170
+ archive = resolve(value);
171
+ } else {
172
+ target = resolve(value);
173
+ }
174
+ index += 1;
175
+ continue;
176
+ }
177
+ throw new Error(`unknown Device Kit installer option: ${argument}`);
178
+ }
179
+ if (!target) {
180
+ throw new Error("--target is required");
181
+ }
182
+ return { archive, target };
183
+ }
184
+
185
+ async function main() {
186
+ const options = parseArgs(process.argv.slice(2));
187
+ let archive = options.archive;
188
+ let downloadDirectory = null;
189
+ try {
190
+ if (!archive) {
191
+ downloadDirectory = mkdtempSync(join(tmpdir(), "tirtc-kit-download-"));
192
+ archive = join(downloadDirectory, ESP32_KIT.archiveName);
193
+ await download(ESP32_KIT.url, archive);
194
+ }
195
+ installEsp32KitArchive(archive, options.target);
196
+ } finally {
197
+ if (downloadDirectory) {
198
+ rmSync(downloadDirectory, { force: true, recursive: true });
199
+ }
200
+ }
201
+ }
202
+
203
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
204
+ main().catch((error) => {
205
+ console.error(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
206
+ process.exitCode = 1;
207
+ });
208
+ }