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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +123 -26
- 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 +7 -6
- package/skills/tirtc-esp32-builder/USAGE.md +16 -11
- package/skills/tirtc-esp32-builder/references/environment.md +25 -4
- package/skills/tirtc-esp32-builder/scripts/doctor.py +4 -4
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
13
|
+
import { ESP32_KIT } from "./esp32-kit-metadata.js";
|
|
14
|
+
|
|
15
|
+
const EXPECTED_IDF_LINE = "5.5";
|
|
16
|
+
const PINNED_IDF_VERSION = "v5.5.4";
|
|
17
|
+
const TARGET = "esp32s3";
|
|
18
|
+
const TARGET_COMPILER = "xtensa-esp32s3-elf-gcc";
|
|
19
|
+
const ESP_IDF_REPOSITORY = "https://github.com/espressif/esp-idf.git";
|
|
20
|
+
const GENERATOR_PATH = join(
|
|
21
|
+
"device-sim",
|
|
22
|
+
"scripts",
|
|
23
|
+
"create_esp32_project.py",
|
|
24
|
+
);
|
|
25
|
+
const SDK_PATH = join(
|
|
26
|
+
"device-sim",
|
|
27
|
+
"sdk",
|
|
28
|
+
"espressif-esp32s3",
|
|
29
|
+
"2.3.0",
|
|
30
|
+
);
|
|
31
|
+
const REQUIRED_SDK_FILES = [
|
|
32
|
+
join("include", "tirtc", "tiRTC.h"),
|
|
33
|
+
join("lib", "libTiRTC.a"),
|
|
34
|
+
join("manifest", "build-contract.env"),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function setupRootFrom(environment) {
|
|
38
|
+
return resolve(
|
|
39
|
+
environment.TIRTC_DEVICE_BUILDER_ROOT ||
|
|
40
|
+
join(homedir(), ".tirtc-device-builder"),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function takeValue(args, index, option) {
|
|
45
|
+
const value = args[index + 1];
|
|
46
|
+
if (!value || value.startsWith("--")) {
|
|
47
|
+
throw new Error(`${option} requires a path`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseSetupOptions(args, defaults = {}) {
|
|
53
|
+
const environment = defaults.environment ?? process.env;
|
|
54
|
+
const options = {
|
|
55
|
+
forceSkill: false,
|
|
56
|
+
help: false,
|
|
57
|
+
idfDir: null,
|
|
58
|
+
install: false,
|
|
59
|
+
kitArchive: null,
|
|
60
|
+
rootDir: setupRootFrom(environment),
|
|
61
|
+
skillsDir: defaults.skillsDir,
|
|
62
|
+
thingConnectRoot: null,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
66
|
+
const argument = args[index];
|
|
67
|
+
if (argument === "--help" || argument === "-h") {
|
|
68
|
+
options.help = true;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (argument === "--install") {
|
|
72
|
+
options.install = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (argument === "--force-skill") {
|
|
76
|
+
options.forceSkill = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
argument === "--root" ||
|
|
81
|
+
argument === "--skills-dir" ||
|
|
82
|
+
argument === "--thing-connect-root" ||
|
|
83
|
+
argument === "--kit-archive" ||
|
|
84
|
+
argument === "--idf-dir"
|
|
85
|
+
) {
|
|
86
|
+
const value = resolve(takeValue(args, index, argument));
|
|
87
|
+
if (argument === "--root") {
|
|
88
|
+
options.rootDir = value;
|
|
89
|
+
} else if (argument === "--skills-dir") {
|
|
90
|
+
options.skillsDir = value;
|
|
91
|
+
} else if (argument === "--thing-connect-root") {
|
|
92
|
+
options.thingConnectRoot = value;
|
|
93
|
+
} else if (argument === "--kit-archive") {
|
|
94
|
+
options.kitArchive = value;
|
|
95
|
+
} else {
|
|
96
|
+
options.idfDir = value;
|
|
97
|
+
}
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`unknown setup option: ${argument}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!options.skillsDir) {
|
|
105
|
+
throw new Error("setup requires a default skills directory");
|
|
106
|
+
}
|
|
107
|
+
if (options.forceSkill && !options.install) {
|
|
108
|
+
throw new Error("--force-skill requires --install");
|
|
109
|
+
}
|
|
110
|
+
if (options.kitArchive && !options.install) {
|
|
111
|
+
throw new Error("--kit-archive requires --install");
|
|
112
|
+
}
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function printSetupHelp(version) {
|
|
117
|
+
console.log(`TiRTC Device Builder ${version}
|
|
118
|
+
|
|
119
|
+
Usage:
|
|
120
|
+
tirtc-device-builder setup esp32 [options]
|
|
121
|
+
|
|
122
|
+
Options:
|
|
123
|
+
--install Install missing user-space components
|
|
124
|
+
--root <path> Managed files (default: ~/.tirtc-device-builder)
|
|
125
|
+
--skills-dir <path> Codex skills directory
|
|
126
|
+
--thing-connect-root <path> Reuse an existing Device Kit or legacy workspace
|
|
127
|
+
--kit-archive <path> Install from a local verified Kit archive
|
|
128
|
+
--idf-dir <path> Reuse or install ESP-IDF at this path
|
|
129
|
+
--force-skill Replace an existing Skill; requires --install
|
|
130
|
+
-h, --help Show this help
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
npx tirtc-device-builder setup esp32
|
|
134
|
+
npx tirtc-device-builder setup esp32 --install
|
|
135
|
+
npx tirtc-device-builder setup esp32 --install --root /opt/tirtc-dev
|
|
136
|
+
|
|
137
|
+
Check mode is read-only. --install downloads the pinned ESP32 Device Kit,
|
|
138
|
+
may clone ESP-IDF v5.5.4, runs Espressif's user-space tool installer, and
|
|
139
|
+
installs the Codex Skill. It does not run sudo or edit shell profiles.`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printCheck(status, name, detail) {
|
|
143
|
+
console.log(`${status.padEnd(5)} ${name}: ${detail}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function commandResult(command, args, options = {}) {
|
|
147
|
+
return spawnSync(command, args, {
|
|
148
|
+
cwd: options.cwd,
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
env: options.environment ?? process.env,
|
|
151
|
+
stdio: options.stdio ?? "pipe",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function commandAvailable(command, args = ["--version"], environment) {
|
|
156
|
+
const result = commandResult(command, args, { environment });
|
|
157
|
+
return !result.error && result.status === 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runOrThrow(command, args, options = {}) {
|
|
161
|
+
console.log(`RUN ${command} ${args.join(" ")}`);
|
|
162
|
+
const result = commandResult(command, args, {
|
|
163
|
+
...options,
|
|
164
|
+
stdio: "inherit",
|
|
165
|
+
});
|
|
166
|
+
if (result.error) {
|
|
167
|
+
throw new Error(`could not run ${command}: ${result.error.message}`);
|
|
168
|
+
}
|
|
169
|
+
if (result.status !== 0) {
|
|
170
|
+
throw new Error(`${command} exited with status ${result.status ?? 1}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeThingConnectRoot(candidate) {
|
|
175
|
+
if (!candidate) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const absolute = resolve(candidate);
|
|
179
|
+
for (const root of [absolute, join(absolute, "thing-connect")]) {
|
|
180
|
+
if (existsSync(join(root, GENERATOR_PATH))) {
|
|
181
|
+
return root;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function discoverThingConnectRoot(start) {
|
|
188
|
+
let candidate = resolve(start);
|
|
189
|
+
for (;;) {
|
|
190
|
+
const found = normalizeThingConnectRoot(candidate);
|
|
191
|
+
if (found) {
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
const parent = dirname(candidate);
|
|
195
|
+
if (parent === candidate) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
candidate = parent;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function thingConnectReady(root) {
|
|
203
|
+
if (!root || !existsSync(join(root, GENERATOR_PATH))) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const sdk = join(root, SDK_PATH);
|
|
207
|
+
return REQUIRED_SDK_FILES.every((relative) =>
|
|
208
|
+
existsSync(join(sdk, relative)),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readConfig(path) {
|
|
213
|
+
if (!existsSync(path)) {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
218
|
+
return value && typeof value === "object" ? value : {};
|
|
219
|
+
} catch {
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function findOnPath(name, environment) {
|
|
225
|
+
const suffixes =
|
|
226
|
+
process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
227
|
+
for (const entry of (environment.PATH || "").split(delimiter)) {
|
|
228
|
+
if (!entry) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
for (const suffix of suffixes) {
|
|
232
|
+
const candidate = join(entry, name + suffix);
|
|
233
|
+
if (existsSync(candidate)) {
|
|
234
|
+
try {
|
|
235
|
+
return realpathSync(candidate);
|
|
236
|
+
} catch {
|
|
237
|
+
return candidate;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function versionMatches(output) {
|
|
246
|
+
const match = output.match(/(?:ESP-IDF\s+)?v?(\d+)\.(\d+)/i);
|
|
247
|
+
return Boolean(match && `${match[1]}.${match[2]}` === EXPECTED_IDF_LINE);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function currentIdf(environment) {
|
|
251
|
+
const idfPath = findOnPath("idf.py", environment);
|
|
252
|
+
const compilerPath = findOnPath(TARGET_COMPILER, environment);
|
|
253
|
+
if (!idfPath) {
|
|
254
|
+
return { ready: false, detail: "idf.py is not active", root: null };
|
|
255
|
+
}
|
|
256
|
+
const result = commandResult(idfPath, ["--version"], { environment });
|
|
257
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
|
258
|
+
let root = null;
|
|
259
|
+
if (
|
|
260
|
+
environment.IDF_PATH &&
|
|
261
|
+
existsSync(join(environment.IDF_PATH, "export.sh"))
|
|
262
|
+
) {
|
|
263
|
+
root = resolve(environment.IDF_PATH);
|
|
264
|
+
} else if (dirname(idfPath).endsWith(join("tools"))) {
|
|
265
|
+
root = dirname(dirname(idfPath));
|
|
266
|
+
}
|
|
267
|
+
if (result.status !== 0 || !versionMatches(output)) {
|
|
268
|
+
return {
|
|
269
|
+
ready: false,
|
|
270
|
+
detail: `${output || "unknown ESP-IDF version"}; expected 5.5.x`,
|
|
271
|
+
root,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (!compilerPath) {
|
|
275
|
+
return {
|
|
276
|
+
ready: false,
|
|
277
|
+
detail: `${TARGET_COMPILER} is not active`,
|
|
278
|
+
root,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { ready: true, detail: output, root };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function activatedResult(idfDir, toolsPath, command, args, options = {}) {
|
|
285
|
+
const environment = { ...process.env, ...options.environment };
|
|
286
|
+
if (toolsPath) {
|
|
287
|
+
environment.IDF_TOOLS_PATH = toolsPath;
|
|
288
|
+
}
|
|
289
|
+
const script =
|
|
290
|
+
'set -e\n. "$1/export.sh" >/dev/null\nshift\nexec "$@"';
|
|
291
|
+
return commandResult(
|
|
292
|
+
"bash",
|
|
293
|
+
["-c", script, "tirtc-device-builder", idfDir, command, ...args],
|
|
294
|
+
{
|
|
295
|
+
environment,
|
|
296
|
+
stdio: options.stdio,
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function idfAtDirectory(idfDir, toolsPath, environment) {
|
|
302
|
+
if (!idfDir || !existsSync(join(idfDir, "export.sh"))) {
|
|
303
|
+
return {
|
|
304
|
+
ready: false,
|
|
305
|
+
detail: `missing ${join(idfDir || "<unset>", "export.sh")}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const version = activatedResult(idfDir, toolsPath, "idf.py", ["--version"], {
|
|
309
|
+
environment,
|
|
310
|
+
});
|
|
311
|
+
const output = `${version.stdout || ""}\n${version.stderr || ""}`.trim();
|
|
312
|
+
if (version.error || version.status !== 0) {
|
|
313
|
+
return {
|
|
314
|
+
ready: false,
|
|
315
|
+
detail: output || version.error?.message || "ESP-IDF activation failed",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (!versionMatches(output)) {
|
|
319
|
+
return {
|
|
320
|
+
ready: false,
|
|
321
|
+
detail: `${output}; expected 5.5.x`,
|
|
322
|
+
wrongVersion: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const compiler = activatedResult(
|
|
326
|
+
idfDir,
|
|
327
|
+
toolsPath,
|
|
328
|
+
TARGET_COMPILER,
|
|
329
|
+
["--version"],
|
|
330
|
+
{ environment },
|
|
331
|
+
);
|
|
332
|
+
if (compiler.error || compiler.status !== 0) {
|
|
333
|
+
return {
|
|
334
|
+
ready: false,
|
|
335
|
+
detail: `${TARGET_COMPILER} is not installed for this ESP-IDF`,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { ready: true, detail: output };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function shellQuote(value) {
|
|
342
|
+
return `'${String(value).replaceAll("'", `'\\''`)}'`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function writeAtomic(path, content, mode) {
|
|
346
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
347
|
+
writeFileSync(temporary, content, { encoding: "utf8", mode });
|
|
348
|
+
renameSync(temporary, path);
|
|
349
|
+
chmodSync(path, mode);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function writeManagedEnvironment(context, packageVersion) {
|
|
353
|
+
mkdirSync(context.rootDir, { recursive: true });
|
|
354
|
+
const config = {
|
|
355
|
+
schema_version: 1,
|
|
356
|
+
platform: "esp32",
|
|
357
|
+
package_version: packageVersion,
|
|
358
|
+
idf_version: PINNED_IDF_VERSION,
|
|
359
|
+
idf_dir: context.idfDir,
|
|
360
|
+
idf_tools_path: context.idfToolsPath,
|
|
361
|
+
device_kit_version: ESP32_KIT.version,
|
|
362
|
+
device_kit_root: context.thingConnectRoot,
|
|
363
|
+
thing_connect_root: context.thingConnectRoot,
|
|
364
|
+
skills_dir: context.skillsDir,
|
|
365
|
+
};
|
|
366
|
+
writeAtomic(
|
|
367
|
+
join(context.rootDir, "config.json"),
|
|
368
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
369
|
+
0o600,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const lines = [
|
|
373
|
+
"# Generated by tirtc-device-builder setup esp32.",
|
|
374
|
+
"# Contains paths only; no device or network credentials.",
|
|
375
|
+
];
|
|
376
|
+
if (context.idfToolsPath) {
|
|
377
|
+
lines.push(
|
|
378
|
+
`export IDF_TOOLS_PATH=${shellQuote(context.idfToolsPath)}`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (context.idfDir && existsSync(join(context.idfDir, "export.sh"))) {
|
|
382
|
+
lines.push(`. ${shellQuote(join(context.idfDir, "export.sh"))}`);
|
|
383
|
+
}
|
|
384
|
+
lines.push(
|
|
385
|
+
`export TIRTC_THING_CONNECT_ROOT=${shellQuote(context.thingConnectRoot)}`,
|
|
386
|
+
);
|
|
387
|
+
writeAtomic(join(context.rootDir, "env.sh"), lines.join("\n") + "\n", 0o600);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function setupContext(options, runtime) {
|
|
391
|
+
const environment = runtime.environment;
|
|
392
|
+
const configPath = join(options.rootDir, "config.json");
|
|
393
|
+
const config = readConfig(configPath);
|
|
394
|
+
const managedThingConnect = join(
|
|
395
|
+
options.rootDir,
|
|
396
|
+
"kits",
|
|
397
|
+
"esp32s3",
|
|
398
|
+
ESP32_KIT.version,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
let requestedThingConnect = null;
|
|
402
|
+
let thingConnectSource = "managed Device Kit";
|
|
403
|
+
if (options.thingConnectRoot) {
|
|
404
|
+
requestedThingConnect = options.thingConnectRoot;
|
|
405
|
+
thingConnectSource = "explicit --thing-connect-root";
|
|
406
|
+
} else if (environment.TIRTC_THING_CONNECT_ROOT) {
|
|
407
|
+
requestedThingConnect = environment.TIRTC_THING_CONNECT_ROOT;
|
|
408
|
+
thingConnectSource = "TIRTC_THING_CONNECT_ROOT";
|
|
409
|
+
} else {
|
|
410
|
+
const discovered = discoverThingConnectRoot(runtime.cwd);
|
|
411
|
+
if (discovered) {
|
|
412
|
+
requestedThingConnect = discovered;
|
|
413
|
+
thingConnectSource = "workspace discovery";
|
|
414
|
+
} else if (config.device_kit_root || config.thing_connect_root) {
|
|
415
|
+
requestedThingConnect = config.device_kit_root || config.thing_connect_root;
|
|
416
|
+
thingConnectSource = "managed config";
|
|
417
|
+
} else {
|
|
418
|
+
requestedThingConnect = managedThingConnect;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const normalizedThingConnect = normalizeThingConnectRoot(
|
|
422
|
+
requestedThingConnect,
|
|
423
|
+
);
|
|
424
|
+
const thingConnectRoot =
|
|
425
|
+
normalizedThingConnect || resolve(requestedThingConnect);
|
|
426
|
+
|
|
427
|
+
const active = currentIdf(environment);
|
|
428
|
+
let idfDir;
|
|
429
|
+
let idfSource;
|
|
430
|
+
if (options.idfDir) {
|
|
431
|
+
idfDir = options.idfDir;
|
|
432
|
+
idfSource = "explicit --idf-dir";
|
|
433
|
+
} else if (active.ready && active.root) {
|
|
434
|
+
idfDir = active.root;
|
|
435
|
+
idfSource = "active ESP-IDF";
|
|
436
|
+
} else if (
|
|
437
|
+
environment.IDF_PATH &&
|
|
438
|
+
existsSync(join(environment.IDF_PATH, "export.sh"))
|
|
439
|
+
) {
|
|
440
|
+
idfDir = resolve(environment.IDF_PATH);
|
|
441
|
+
idfSource = "IDF_PATH";
|
|
442
|
+
} else if (config.idf_dir) {
|
|
443
|
+
idfDir = resolve(config.idf_dir);
|
|
444
|
+
idfSource = "managed config";
|
|
445
|
+
} else {
|
|
446
|
+
idfDir = join(options.rootDir, `esp-idf-${PINNED_IDF_VERSION}`);
|
|
447
|
+
idfSource = "managed";
|
|
448
|
+
}
|
|
449
|
+
const idfToolsPath =
|
|
450
|
+
environment.IDF_TOOLS_PATH ||
|
|
451
|
+
config.idf_tools_path ||
|
|
452
|
+
(idfSource === "managed" ? join(options.rootDir, "espressif") : null);
|
|
453
|
+
const directoryIdf = idfAtDirectory(
|
|
454
|
+
idfDir,
|
|
455
|
+
idfToolsPath,
|
|
456
|
+
environment,
|
|
457
|
+
);
|
|
458
|
+
const idf = idfSource === "active ESP-IDF" && active.ready
|
|
459
|
+
? { ...active, directoryReady: directoryIdf.ready }
|
|
460
|
+
: { ...directoryIdf, root: idfDir, directoryReady: directoryIdf.ready };
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
activeIdf: active,
|
|
464
|
+
configPath,
|
|
465
|
+
idf,
|
|
466
|
+
idfDir,
|
|
467
|
+
idfSource,
|
|
468
|
+
idfToolsPath,
|
|
469
|
+
managedThingConnect,
|
|
470
|
+
rootDir: options.rootDir,
|
|
471
|
+
skillTarget: join(options.skillsDir, runtime.platform.skill),
|
|
472
|
+
skillsDir: options.skillsDir,
|
|
473
|
+
thingConnectReady: thingConnectReady(thingConnectRoot),
|
|
474
|
+
thingConnectRoot,
|
|
475
|
+
thingConnectSource,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function printState(context, runtime) {
|
|
480
|
+
printCheck("INFO", "setup root", context.rootDir);
|
|
481
|
+
printCheck(
|
|
482
|
+
existsSync(join(context.skillTarget, "SKILL.md")) ? "PASS" : "MISS",
|
|
483
|
+
"Codex Skill",
|
|
484
|
+
context.skillTarget,
|
|
485
|
+
);
|
|
486
|
+
printCheck(
|
|
487
|
+
context.thingConnectReady ? "PASS" : "MISS",
|
|
488
|
+
"ESP32 Device Kit",
|
|
489
|
+
`${context.thingConnectRoot} (${context.thingConnectSource})`,
|
|
490
|
+
);
|
|
491
|
+
printCheck(
|
|
492
|
+
context.idf.ready ? "PASS" : "MISS",
|
|
493
|
+
"ESP-IDF",
|
|
494
|
+
`${context.idf.detail} (${context.idfSource}: ${context.idfDir})`,
|
|
495
|
+
);
|
|
496
|
+
for (const command of ["python3", "git", "bash", "tar"]) {
|
|
497
|
+
printCheck(
|
|
498
|
+
commandAvailable(command, ["--version"], runtime.environment)
|
|
499
|
+
? "PASS"
|
|
500
|
+
: "MISS",
|
|
501
|
+
command,
|
|
502
|
+
findOnPath(command, runtime.environment) || "not found",
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function prerequisites(runtime) {
|
|
508
|
+
const checks = [
|
|
509
|
+
["python3", ["--version"]],
|
|
510
|
+
["git", ["--version"]],
|
|
511
|
+
["bash", ["--version"]],
|
|
512
|
+
["tar", ["--version"]],
|
|
513
|
+
];
|
|
514
|
+
return checks
|
|
515
|
+
.filter(
|
|
516
|
+
([command, args]) =>
|
|
517
|
+
!commandAvailable(command, args, runtime.environment),
|
|
518
|
+
)
|
|
519
|
+
.map(([command]) => command);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function printSystemDependencyHelp(missing) {
|
|
523
|
+
console.error(
|
|
524
|
+
`BLOCKED: missing system prerequisite(s): ${missing.join(", ")}`,
|
|
525
|
+
);
|
|
526
|
+
console.error("Ubuntu/Debian/WSL example:");
|
|
527
|
+
console.error(
|
|
528
|
+
" sudo apt-get update && sudo apt-get install -y git python3 python3-venv",
|
|
529
|
+
);
|
|
530
|
+
console.error(
|
|
531
|
+
"Install system packages yourself, then rerun setup; this command never runs sudo.",
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function installSkill(options, context, runtime) {
|
|
536
|
+
const present = existsSync(join(context.skillTarget, "SKILL.md"));
|
|
537
|
+
if (present && !options.forceSkill) {
|
|
538
|
+
console.log(`SKIP Codex Skill already exists: ${context.skillTarget}`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const args = [
|
|
542
|
+
runtime.cliPath,
|
|
543
|
+
"install",
|
|
544
|
+
runtime.platform.name,
|
|
545
|
+
"--skills-dir",
|
|
546
|
+
options.skillsDir,
|
|
547
|
+
];
|
|
548
|
+
if (options.forceSkill) {
|
|
549
|
+
args.push("--force");
|
|
550
|
+
}
|
|
551
|
+
runOrThrow(process.execPath, args, {
|
|
552
|
+
environment: runtime.environment,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function installDeviceKit(options, context, runtime) {
|
|
557
|
+
if (context.thingConnectReady) {
|
|
558
|
+
console.log(
|
|
559
|
+
`SKIP ESP32 Device Kit already ready: ${context.thingConnectRoot}`,
|
|
560
|
+
);
|
|
561
|
+
return context.thingConnectRoot;
|
|
562
|
+
}
|
|
563
|
+
if (context.thingConnectSource !== "managed Device Kit") {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`${context.thingConnectSource} does not contain a complete ESP32 Device Kit: ${context.thingConnectRoot}`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (existsSync(context.managedThingConnect)) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`refusing to overwrite incomplete directory: ${context.managedThingConnect}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
const installer = join(runtime.packageRoot, "bin", "install-esp32-kit.js");
|
|
574
|
+
const args = [installer, "--target", context.managedThingConnect];
|
|
575
|
+
if (options.kitArchive) {
|
|
576
|
+
args.push("--archive", options.kitArchive);
|
|
577
|
+
}
|
|
578
|
+
runOrThrow(process.execPath, args, { environment: runtime.environment });
|
|
579
|
+
const root = normalizeThingConnectRoot(context.managedThingConnect);
|
|
580
|
+
if (!thingConnectReady(root)) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`installed ESP32 Device Kit is missing its generator or TiRTC SDK: ${context.managedThingConnect}`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
return root;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function installIdf(context, runtime) {
|
|
589
|
+
if (context.idf.ready) {
|
|
590
|
+
console.log(`SKIP ESP-IDF already ready: ${context.idfDir}`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (process.platform === "win32") {
|
|
594
|
+
throw new Error(
|
|
595
|
+
"automatic ESP-IDF installation currently supports Linux, WSL, and macOS; use Espressif's Windows installer and rerun setup",
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (context.idf.wrongVersion && existsSync(context.idfDir)) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`existing ESP-IDF at ${context.idfDir} does not match 5.5.x; select another --idf-dir`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (!existsSync(join(context.idfDir, "export.sh"))) {
|
|
604
|
+
if (existsSync(context.idfDir)) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`refusing to overwrite incomplete directory: ${context.idfDir}`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
mkdirSync(dirname(context.idfDir), { recursive: true });
|
|
610
|
+
runOrThrow(
|
|
611
|
+
"git",
|
|
612
|
+
[
|
|
613
|
+
"clone",
|
|
614
|
+
"--branch",
|
|
615
|
+
PINNED_IDF_VERSION,
|
|
616
|
+
"--depth",
|
|
617
|
+
"1",
|
|
618
|
+
"--recursive",
|
|
619
|
+
"--shallow-submodules",
|
|
620
|
+
ESP_IDF_REPOSITORY,
|
|
621
|
+
context.idfDir,
|
|
622
|
+
],
|
|
623
|
+
{ environment: runtime.environment },
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const installEnvironment = { ...runtime.environment };
|
|
627
|
+
if (context.idfToolsPath) {
|
|
628
|
+
installEnvironment.IDF_TOOLS_PATH = context.idfToolsPath;
|
|
629
|
+
}
|
|
630
|
+
runOrThrow("bash", [join(context.idfDir, "install.sh"), TARGET], {
|
|
631
|
+
environment: installEnvironment,
|
|
632
|
+
});
|
|
633
|
+
const ready = idfAtDirectory(
|
|
634
|
+
context.idfDir,
|
|
635
|
+
context.idfToolsPath,
|
|
636
|
+
runtime.environment,
|
|
637
|
+
);
|
|
638
|
+
if (!ready.ready) {
|
|
639
|
+
throw new Error(`ESP-IDF installation did not become ready: ${ready.detail}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function runDoctor(context, runtime) {
|
|
644
|
+
const doctor = join(
|
|
645
|
+
runtime.packageRoot,
|
|
646
|
+
"skills",
|
|
647
|
+
runtime.platform.skill,
|
|
648
|
+
"scripts",
|
|
649
|
+
"doctor.py",
|
|
650
|
+
);
|
|
651
|
+
const args = [
|
|
652
|
+
doctor,
|
|
653
|
+
"--expected-idf",
|
|
654
|
+
EXPECTED_IDF_LINE,
|
|
655
|
+
"--target",
|
|
656
|
+
TARGET,
|
|
657
|
+
"--thing-connect-root",
|
|
658
|
+
context.thingConnectRoot,
|
|
659
|
+
"--require-workspace",
|
|
660
|
+
];
|
|
661
|
+
let result;
|
|
662
|
+
if (context.idfDir && existsSync(join(context.idfDir, "export.sh"))) {
|
|
663
|
+
result = activatedResult(
|
|
664
|
+
context.idfDir,
|
|
665
|
+
context.idfToolsPath,
|
|
666
|
+
runtime.environment.PYTHON || "python3",
|
|
667
|
+
args,
|
|
668
|
+
{
|
|
669
|
+
environment: {
|
|
670
|
+
...runtime.environment,
|
|
671
|
+
TIRTC_THING_CONNECT_ROOT: context.thingConnectRoot,
|
|
672
|
+
},
|
|
673
|
+
stdio: "inherit",
|
|
674
|
+
},
|
|
675
|
+
);
|
|
676
|
+
} else {
|
|
677
|
+
result = commandResult(runtime.environment.PYTHON || "python3", args, {
|
|
678
|
+
environment: {
|
|
679
|
+
...runtime.environment,
|
|
680
|
+
TIRTC_THING_CONNECT_ROOT: context.thingConnectRoot,
|
|
681
|
+
},
|
|
682
|
+
stdio: "inherit",
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (result.error) {
|
|
686
|
+
console.error(`ERROR: could not run Doctor: ${result.error.message}`);
|
|
687
|
+
return 1;
|
|
688
|
+
}
|
|
689
|
+
return Number.isInteger(result.status) ? result.status : 1;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function isReady(context) {
|
|
693
|
+
return (
|
|
694
|
+
existsSync(join(context.skillTarget, "SKILL.md")) &&
|
|
695
|
+
context.thingConnectReady &&
|
|
696
|
+
context.idf.ready
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function runEsp32Setup(args, input) {
|
|
701
|
+
const runtime = {
|
|
702
|
+
cliPath: input.cliPath,
|
|
703
|
+
cwd: input.cwd ?? process.cwd(),
|
|
704
|
+
environment: input.environment ?? process.env,
|
|
705
|
+
packageRoot: input.packageRoot,
|
|
706
|
+
packageVersion: input.packageVersion,
|
|
707
|
+
platform: input.platform,
|
|
708
|
+
};
|
|
709
|
+
let options;
|
|
710
|
+
try {
|
|
711
|
+
options = parseSetupOptions(args, {
|
|
712
|
+
environment: runtime.environment,
|
|
713
|
+
skillsDir: input.defaultSkillsDir,
|
|
714
|
+
});
|
|
715
|
+
} catch (error) {
|
|
716
|
+
console.error(
|
|
717
|
+
`ERROR: ${error instanceof Error ? error.message : String(error)}`,
|
|
718
|
+
);
|
|
719
|
+
return 1;
|
|
720
|
+
}
|
|
721
|
+
if (options.help) {
|
|
722
|
+
printSetupHelp(runtime.packageVersion);
|
|
723
|
+
return 0;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let context = setupContext(options, runtime);
|
|
727
|
+
printState(context, runtime);
|
|
728
|
+
if (!options.install) {
|
|
729
|
+
if (!isReady(context)) {
|
|
730
|
+
console.log("OVERALL: NEEDS_SETUP");
|
|
731
|
+
console.log(
|
|
732
|
+
"NEXT: npx tirtc-device-builder@latest setup esp32 --install",
|
|
733
|
+
);
|
|
734
|
+
return 1;
|
|
735
|
+
}
|
|
736
|
+
const status = runDoctor(context, runtime);
|
|
737
|
+
console.log(
|
|
738
|
+
status === 0 ? "SETUP: READY" : "SETUP: Doctor reported a blocker",
|
|
739
|
+
);
|
|
740
|
+
return status;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const missing = prerequisites(runtime);
|
|
744
|
+
if (missing.length > 0) {
|
|
745
|
+
printSystemDependencyHelp(missing);
|
|
746
|
+
return 1;
|
|
747
|
+
}
|
|
748
|
+
console.log("INSTALL PLAN:");
|
|
749
|
+
console.log(` Skill: ${context.skillTarget}`);
|
|
750
|
+
console.log(` Device Kit: ${context.thingConnectRoot} (${ESP32_KIT.version})`);
|
|
751
|
+
console.log(` ESP-IDF: ${context.idfDir} (${PINNED_IDF_VERSION})`);
|
|
752
|
+
console.log(` IDF tools: ${context.idfToolsPath || "existing default"}`);
|
|
753
|
+
console.log(" System sudo: never");
|
|
754
|
+
console.log(" Shell profile: unchanged");
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
installSkill(options, context, runtime);
|
|
758
|
+
const thingConnectRoot = installDeviceKit(options, context, runtime);
|
|
759
|
+
context = { ...context, thingConnectRoot, thingConnectReady: true };
|
|
760
|
+
installIdf(context, runtime);
|
|
761
|
+
context = setupContext(options, {
|
|
762
|
+
...runtime,
|
|
763
|
+
environment: {
|
|
764
|
+
...runtime.environment,
|
|
765
|
+
TIRTC_THING_CONNECT_ROOT: thingConnectRoot,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
if (!context.idf.ready) {
|
|
769
|
+
const directoryIdf = idfAtDirectory(
|
|
770
|
+
context.idfDir,
|
|
771
|
+
context.idfToolsPath,
|
|
772
|
+
runtime.environment,
|
|
773
|
+
);
|
|
774
|
+
context = { ...context, idf: directoryIdf };
|
|
775
|
+
}
|
|
776
|
+
writeManagedEnvironment(context, runtime.packageVersion);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
console.error(
|
|
779
|
+
`ERROR: ${error instanceof Error ? error.message : String(error)}`,
|
|
780
|
+
);
|
|
781
|
+
console.error(
|
|
782
|
+
"The setup is resumable. Fix the reported item and rerun the same command.",
|
|
783
|
+
);
|
|
784
|
+
return 1;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const status = runDoctor(context, runtime);
|
|
788
|
+
if (status !== 0) {
|
|
789
|
+
console.error("SETUP: installation finished, but Doctor still reports a blocker");
|
|
790
|
+
return status;
|
|
791
|
+
}
|
|
792
|
+
console.log("SETUP: READY");
|
|
793
|
+
console.log(`Environment helper: ${join(context.rootDir, "env.sh")}`);
|
|
794
|
+
console.log("Start a new Codex session and invoke $tirtc-esp32-builder.");
|
|
795
|
+
return 0;
|
|
796
|
+
}
|