tirtc-device-builder 0.2.0 → 0.4.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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +657 -362
- package/bin/esp32-kit-metadata.js +8 -0
- package/bin/install-esp32-kit.js +208 -0
- package/bin/setup-esp32.js +796 -0
- package/bin/tirtc-device-builder.js +19 -2
- package/package.json +2 -1
- package/skills/tirtc-esp32-builder/SKILL.md +11 -8
- package/skills/tirtc-esp32-builder/USAGE.md +30 -22
- package/skills/tirtc-esp32-builder/assets/developer-intake-prompt.md +50 -0
- package/skills/tirtc-esp32-builder/assets/hardware-ir-v2.example.json +122 -0
- package/skills/tirtc-esp32-builder/assets/report-template.md +15 -0
- package/skills/tirtc-esp32-builder/references/capability-rules.md +41 -16
- package/skills/tirtc-esp32-builder/references/environment.md +25 -4
- package/skills/tirtc-esp32-builder/references/hardware-ir.md +44 -24
- package/skills/tirtc-esp32-builder/references/porting-risks.md +49 -0
- package/skills/tirtc-esp32-builder/references/reporting.md +5 -1
- package/skills/tirtc-esp32-builder/references/workflow.md +10 -9
- package/skills/tirtc-esp32-builder/scripts/doctor.py +4 -4
- package/skills/tirtc-esp32-builder/scripts/hardware_ir.py +670 -40
|
@@ -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
|
+
}
|