tirtc-device-builder 0.7.0 → 0.7.2

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.7.0",
3
+ "version": "0.7.2",
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.7.2
6
+
7
+ - Require the selected managed ESP32 Device Kit manifest to match the pinned Kit version; ignore stale managed references instead of reporting an older structurally complete Kit as ready.
8
+ - Make Doctor validate an exact `--expected-kit` version and persist the version actually read from the selected Kit rather than the desired version.
9
+ - Ship a verifiable Skill `VERSION` marker, include it in setup readiness and package tests, and remove the inapplicable Plugin-version prerequisite from the portable clean-room prompt.
10
+
11
+ ## 0.7.1
12
+
13
+ - Split clean-room bootstrap from Skill execution: install or replace the pinned Skill before Codex starts, restart the session, then run read-only version and Doctor checks before generation.
14
+
5
15
  ## 0.7.0
6
16
 
7
17
  - Separate the platform/Web video contract (MJPEG, H.264, and H.265 on stream 11) from the single codec profile selected by a board; keep the LCKFB ESP32-S3 adapter on MJPEG without narrowing platform capability.
package/README.md CHANGED
@@ -881,8 +881,8 @@ metadata 中的版本、标签、上游 commit 和期望 SHA-256 必须与本地
881
881
 
882
882
  ```bash
883
883
  npm test
884
- git tag -a v0.7.0 -m "v0.7.0"
885
- git push origin v0.7.0
884
+ git tag -a v0.7.2 -m "v0.7.2"
885
+ git push origin v0.7.2
886
886
  ```
887
887
 
888
888
  不要重复发布已经存在的 npm 版本。版本变化同步更新 `package.json`、`.codex-plugin/plugin.json` 和发布说明。
@@ -33,6 +33,8 @@ const REQUIRED_SDK_FILES = [
33
33
  join("lib", "libTiRTC.a"),
34
34
  join("manifest", "build-contract.env"),
35
35
  ];
36
+ const DEVICE_KIT_MANIFEST = "manifest.json";
37
+ const SKILL_VERSION_FILE = "VERSION";
36
38
 
37
39
  function setupRootFrom(environment) {
38
40
  return resolve(
@@ -199,14 +201,71 @@ function discoverThingConnectRoot(start) {
199
201
  }
200
202
  }
201
203
 
202
- function thingConnectReady(root) {
203
- if (!root || !existsSync(join(root, GENERATOR_PATH))) {
204
- return false;
204
+ function readTrimmedFile(path) {
205
+ if (!existsSync(path)) {
206
+ return null;
205
207
  }
206
- const sdk = join(root, SDK_PATH);
207
- return REQUIRED_SDK_FILES.every((relative) =>
208
- existsSync(join(sdk, relative)),
208
+ try {
209
+ const value = readFileSync(path, "utf8").trim();
210
+ return value || null;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ export function inspectDeviceKit(root) {
217
+ const manifestPath = root ? join(root, DEVICE_KIT_MANIFEST) : null;
218
+ let manifestPresent = false;
219
+ let version = null;
220
+ let manifestError = null;
221
+ if (manifestPath && existsSync(manifestPath)) {
222
+ manifestPresent = true;
223
+ try {
224
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
225
+ if (
226
+ manifest &&
227
+ typeof manifest === "object" &&
228
+ typeof manifest.kit_version === "string" &&
229
+ manifest.kit_version.trim()
230
+ ) {
231
+ version = manifest.kit_version.trim();
232
+ } else {
233
+ manifestError = "manifest.json does not declare kit_version";
234
+ }
235
+ } catch (error) {
236
+ manifestError = `invalid manifest.json: ${
237
+ error instanceof Error ? error.message : String(error)
238
+ }`;
239
+ }
240
+ }
241
+
242
+ const generatorReady = Boolean(
243
+ root && existsSync(join(root, GENERATOR_PATH)),
209
244
  );
245
+ const sdk = root ? join(root, SDK_PATH) : null;
246
+ const sdkReady = Boolean(
247
+ sdk &&
248
+ REQUIRED_SDK_FILES.every((relative) => existsSync(join(sdk, relative))),
249
+ );
250
+ const structureReady = generatorReady && sdkReady;
251
+ const versionCompatible =
252
+ !manifestPresent ||
253
+ (manifestError === null && version === ESP32_KIT.version);
254
+
255
+ return {
256
+ expectedVersion: ESP32_KIT.version,
257
+ manifestError,
258
+ manifestPath,
259
+ manifestPresent,
260
+ ready: structureReady && versionCompatible,
261
+ structureReady,
262
+ version,
263
+ versionCompatible,
264
+ };
265
+ }
266
+
267
+ export function readSkillVersion(skillRoot) {
268
+ return readTrimmedFile(join(skillRoot, SKILL_VERSION_FILE));
210
269
  }
211
270
 
212
271
  function readConfig(path) {
@@ -358,7 +417,7 @@ function writeManagedEnvironment(context, packageVersion) {
358
417
  idf_version: PINNED_IDF_VERSION,
359
418
  idf_dir: context.idfDir,
360
419
  idf_tools_path: context.idfToolsPath,
361
- device_kit_version: ESP32_KIT.version,
420
+ device_kit_version: context.deviceKit.version,
362
421
  device_kit_root: context.thingConnectRoot,
363
422
  thing_connect_root: context.thingConnectRoot,
364
423
  skills_dir: context.skillsDir,
@@ -418,11 +477,31 @@ function setupContext(options, runtime) {
418
477
  requestedThingConnect = managedThingConnect;
419
478
  }
420
479
  }
421
- const normalizedThingConnect = normalizeThingConnectRoot(
480
+ let normalizedThingConnect = normalizeThingConnectRoot(
422
481
  requestedThingConnect,
423
482
  );
424
- const thingConnectRoot =
483
+ let thingConnectRoot =
425
484
  normalizedThingConnect || resolve(requestedThingConnect);
485
+ let deviceKit = inspectDeviceKit(thingConnectRoot);
486
+ let thingConnectFallback = null;
487
+ const versionedKitMismatch =
488
+ deviceKit.manifestPresent && !deviceKit.versionCompatible;
489
+ const staleManagedReference =
490
+ (thingConnectSource === "TIRTC_THING_CONNECT_ROOT" ||
491
+ thingConnectSource === "managed config") &&
492
+ !deviceKit.structureReady;
493
+ if (
494
+ thingConnectSource !== "explicit --thing-connect-root" &&
495
+ (versionedKitMismatch || staleManagedReference)
496
+ ) {
497
+ thingConnectFallback = `${thingConnectSource}: ${thingConnectRoot}`;
498
+ requestedThingConnect = managedThingConnect;
499
+ thingConnectSource = "managed Device Kit";
500
+ normalizedThingConnect = normalizeThingConnectRoot(requestedThingConnect);
501
+ thingConnectRoot =
502
+ normalizedThingConnect || resolve(requestedThingConnect);
503
+ deviceKit = inspectDeviceKit(thingConnectRoot);
504
+ }
426
505
 
427
506
  const active = currentIdf(environment);
428
507
  let idfDir;
@@ -459,6 +538,8 @@ function setupContext(options, runtime) {
459
538
  ? { ...active, directoryReady: directoryIdf.ready }
460
539
  : { ...directoryIdf, root: idfDir, directoryReady: directoryIdf.ready };
461
540
 
541
+ const skillTarget = join(options.skillsDir, runtime.platform.skill);
542
+ const skillVersion = readSkillVersion(skillTarget);
462
543
  return {
463
544
  activeIdf: active,
464
545
  configPath,
@@ -468,9 +549,17 @@ function setupContext(options, runtime) {
468
549
  idfToolsPath,
469
550
  managedThingConnect,
470
551
  rootDir: options.rootDir,
471
- skillTarget: join(options.skillsDir, runtime.platform.skill),
552
+ deviceKit,
553
+ skillReady:
554
+ existsSync(join(skillTarget, "SKILL.md")) &&
555
+ skillVersion === runtime.packageVersion,
556
+ skillTarget,
557
+ skillVersion,
472
558
  skillsDir: options.skillsDir,
473
- thingConnectReady: thingConnectReady(thingConnectRoot),
559
+ thingConnectFallback,
560
+ thingConnectManaged:
561
+ resolve(thingConnectRoot) === resolve(managedThingConnect),
562
+ thingConnectReady: deviceKit.ready,
474
563
  thingConnectRoot,
475
564
  thingConnectSource,
476
565
  };
@@ -479,15 +568,29 @@ function setupContext(options, runtime) {
479
568
  function printState(context, runtime) {
480
569
  printCheck("INFO", "setup root", context.rootDir);
481
570
  printCheck(
482
- existsSync(join(context.skillTarget, "SKILL.md")) ? "PASS" : "MISS",
571
+ context.skillReady ? "PASS" : "MISS",
483
572
  "Codex Skill",
484
- context.skillTarget,
573
+ `${context.skillTarget} (version ${
574
+ context.skillVersion || "missing"
575
+ }; expected ${runtime.packageVersion})`,
485
576
  );
577
+ const kitVersion = context.deviceKit.manifestPresent
578
+ ? context.deviceKit.version || "invalid manifest"
579
+ : "legacy workspace (unversioned)";
486
580
  printCheck(
487
581
  context.thingConnectReady ? "PASS" : "MISS",
488
582
  "ESP32 Device Kit",
489
- `${context.thingConnectRoot} (${context.thingConnectSource})`,
583
+ `${context.thingConnectRoot} (${context.thingConnectSource}; version ${
584
+ kitVersion
585
+ }; expected ${ESP32_KIT.version})`,
490
586
  );
587
+ if (context.thingConnectFallback) {
588
+ printCheck(
589
+ "INFO",
590
+ "ignored stale Kit reference",
591
+ context.thingConnectFallback,
592
+ );
593
+ }
491
594
  printCheck(
492
595
  context.idf.ready ? "PASS" : "MISS",
493
596
  "ESP-IDF",
@@ -534,10 +637,19 @@ function printSystemDependencyHelp(missing) {
534
637
 
535
638
  function installSkill(options, context, runtime) {
536
639
  const present = existsSync(join(context.skillTarget, "SKILL.md"));
537
- if (present && !options.forceSkill) {
538
- console.log(`SKIP Codex Skill already exists: ${context.skillTarget}`);
640
+ if (present && context.skillReady && !options.forceSkill) {
641
+ console.log(
642
+ `SKIP Codex Skill ${runtime.packageVersion} already exists: ${context.skillTarget}`,
643
+ );
539
644
  return;
540
645
  }
646
+ if (present && !options.forceSkill) {
647
+ throw new Error(
648
+ `Codex Skill at ${context.skillTarget} has version ${
649
+ context.skillVersion || "missing"
650
+ }, expected ${runtime.packageVersion}; rerun with --force-skill to replace it`,
651
+ );
652
+ }
541
653
  const args = [
542
654
  runtime.cliPath,
543
655
  "install",
@@ -560,9 +672,14 @@ function installDeviceKit(options, context, runtime) {
560
672
  );
561
673
  return context.thingConnectRoot;
562
674
  }
563
- if (context.thingConnectSource !== "managed Device Kit") {
675
+ if (!context.thingConnectManaged) {
676
+ const reason =
677
+ context.deviceKit.manifestError ||
678
+ (context.deviceKit.manifestPresent
679
+ ? `version ${context.deviceKit.version || "missing"}; expected ${ESP32_KIT.version}`
680
+ : "missing generator or TiRTC SDK");
564
681
  throw new Error(
565
- `${context.thingConnectSource} does not contain a complete ESP32 Device Kit: ${context.thingConnectRoot}`,
682
+ `${context.thingConnectSource} is not a compatible ESP32 Device Kit (${reason}): ${context.thingConnectRoot}`,
566
683
  );
567
684
  }
568
685
  if (existsSync(context.managedThingConnect)) {
@@ -577,9 +694,10 @@ function installDeviceKit(options, context, runtime) {
577
694
  }
578
695
  runOrThrow(process.execPath, args, { environment: runtime.environment });
579
696
  const root = normalizeThingConnectRoot(context.managedThingConnect);
580
- if (!thingConnectReady(root)) {
697
+ const installedKit = inspectDeviceKit(root);
698
+ if (!installedKit.ready || installedKit.version !== ESP32_KIT.version) {
581
699
  throw new Error(
582
- `installed ESP32 Device Kit is missing its generator or TiRTC SDK: ${context.managedThingConnect}`,
700
+ `installed ESP32 Device Kit is incomplete or has the wrong version: ${context.managedThingConnect}`,
583
701
  );
584
702
  }
585
703
  return root;
@@ -658,6 +776,9 @@ function runDoctor(context, runtime) {
658
776
  context.thingConnectRoot,
659
777
  "--require-workspace",
660
778
  ];
779
+ if (context.deviceKit.manifestPresent) {
780
+ args.push("--expected-kit", ESP32_KIT.version);
781
+ }
661
782
  let result;
662
783
  if (context.idfDir && existsSync(join(context.idfDir, "export.sh"))) {
663
784
  result = activatedResult(
@@ -691,7 +812,7 @@ function runDoctor(context, runtime) {
691
812
 
692
813
  function isReady(context) {
693
814
  return (
694
- existsSync(join(context.skillTarget, "SKILL.md")) &&
815
+ context.skillReady &&
695
816
  context.thingConnectReady &&
696
817
  context.idf.ready
697
818
  );
@@ -729,7 +850,7 @@ export function runEsp32Setup(args, input) {
729
850
  if (!isReady(context)) {
730
851
  console.log("OVERALL: NEEDS_SETUP");
731
852
  console.log(
732
- "NEXT: npx tirtc-device-builder@latest setup esp32 --install",
853
+ `NEXT: npx tirtc-device-builder@${runtime.packageVersion} setup esp32 --install`,
733
854
  );
734
855
  return 1;
735
856
  }
@@ -756,7 +877,12 @@ export function runEsp32Setup(args, input) {
756
877
  try {
757
878
  installSkill(options, context, runtime);
758
879
  const thingConnectRoot = installDeviceKit(options, context, runtime);
759
- context = { ...context, thingConnectRoot, thingConnectReady: true };
880
+ context = {
881
+ ...context,
882
+ deviceKit: inspectDeviceKit(thingConnectRoot),
883
+ thingConnectRoot,
884
+ thingConnectReady: true,
885
+ };
760
886
  installIdf(context, runtime);
761
887
  context = setupContext(options, {
762
888
  ...runtime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-device-builder",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Install and run TiRTC device-development skills for Codex.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -9,9 +9,9 @@ Turn board evidence into an evidence-backed ESP-IDF project. Treat the Hardware
9
9
 
10
10
  ## Start
11
11
 
12
- 1. Read [environment.md](references/environment.md). For a first-time setup or a request to check/install prerequisites, prefer `npx tirtc-device-builder@latest setup esp32`. Run its `--install` mode only when the user explicitly authorizes installation at the displayed destinations. The installer may create user-space files but never grants permission for `sudo` or persistent shell-profile edits.
13
- 2. Locate the versioned ESP32 Device Kit root containing `device-sim/` using the explicit input, `TIRTC_THING_CONNECT_ROOT`, the managed setup configuration, or workspace discovery. Treat its manifest and packaged protocol documents as the generation facts. If the user explicitly supplies a full ThingConnect source workspace, also read its applicable `AGENTS.md`.
14
- 3. Run the Doctor through the managed environment helper when one exists; otherwise run `python3 <skill-dir>/scripts/doctor.py --expected-idf 5.5 --target esp32s3`. Add `--require-workspace` when generation or repository reference documents are needed; a self-contained generated project can instead resolve its bundled SDK through `--project`. Resolve every required failure before claiming build readiness.
12
+ 1. Read this Skill's `VERSION` file and [environment.md](references/environment.md). Record the `VERSION` value as the installed Skill version; never infer it from the prompt or an npm command run elsewhere. For a first-time setup or a request to check/install prerequisites, prefer `npx tirtc-device-builder@latest setup esp32`. Run its `--install` mode only when the user explicitly authorizes installation at the displayed destinations. The installer may create user-space files but never grants permission for `sudo` or persistent shell-profile edits.
13
+ 2. Locate the versioned ESP32 Device Kit root containing `device-sim/` using the explicit input, `TIRTC_THING_CONNECT_ROOT`, the managed setup configuration, or workspace discovery. For a managed Kit, read `manifest.json`, require its exact expected `kit_version`, and treat its manifest and packaged protocol documents as generation facts. An older but structurally complete Kit is not compatible. If the user explicitly supplies an unversioned full ThingConnect source workspace, treat it as legacy input and also read its applicable `AGENTS.md`.
14
+ 3. Run the Doctor through the managed environment helper when one exists; otherwise run `python3 <skill-dir>/scripts/doctor.py --expected-idf 5.5 --target esp32s3`. Add `--expected-kit <kit-version> --require-workspace` for managed-Kit generation; a self-contained generated project can instead resolve its bundled SDK through `--project`. Resolve every required failure before claiming build readiness.
15
15
  4. Read [workflow.md](references/workflow.md). Select the registered-board, new-board intake, or existing-project branch. The branch is selected when every supplied artifact has been accounted for and the exact board revision is known or explicitly unresolved.
16
16
  5. Read [hardware-ir.md](references/hardware-ir.md) when a Hardware IR must be created or updated. New intake uses schema v2; schema v1 remains readable for existing H.264 projects. Record a source and verification level for every hardware fact that affects a requested feature.
17
17
  6. Run `python3 <skill-dir>/scripts/hardware_ir.py validate <hardware-ir.json>` and then `assess --phase intake --strict`. `READY_TO_PORT` means the evidence is sufficient to design the adapter without guessing wiring or changing an unapproved public contract; it does not require a final ELF or runtime measurements.
@@ -0,0 +1 @@
1
+ 0.7.2
@@ -2,13 +2,24 @@ $tirtc-esp32-builder
2
2
 
3
3
  请在当前工作区完成一次立创·实战派 ESP32-S3 的 ThingConnect TiRTC clean-room L-1/L0/L1 接入验证。
4
4
 
5
- 这是 Skill、提示词和开发板资料的独立有效性测试。允许访问外网。先运行:
5
+ 这是 Skill、提示词和开发板资料的独立有效性测试。允许访问外网。
6
+
7
+ 前置条件(必须由开发者在启动本次 Codex 会话前完成,不属于本提示词内的操作):
8
+
9
+ ```bash
10
+ npx --yes tirtc-device-builder@0.7.2 setup esp32 --install --force-skill
11
+ ```
12
+
13
+ 该命令只允许在当前用户目录安装固定版本的 Skill、managed ESP32 Device Kit、ESP-IDF 和工具链;禁止 sudo、系统级包变更和修改 shell profile。安装完成后,开发者必须关闭原 Codex 会话,再从本 clean-room 工作区启动一个新会话,然后粘贴本提示词。
14
+
15
+ 本轮第一步先只读运行:
6
16
 
7
17
  ```bash
8
- npx --yes tirtc-device-builder@0.7.0 setup esp32 --install
18
+ npx --yes tirtc-device-builder@0.7.2 --version
19
+ npx --yes tirtc-device-builder@0.7.2 setup esp32
9
20
  ```
10
21
 
11
- 允许该命令在当前用户目录安装固定版本的 Skill、managed ESP32 Device Kit、ESP-IDF 和工具链;禁止 sudo、系统级包变更和修改 shell profile。安装后必须使用 managed 环境继续执行。
22
+ 必须根据命令的实际输出和本机文件确认:npm 包为 0.7.2、已安装 Skill `VERSION` 为 0.7.2、所选 Device Kit 的 `manifest.json` 中 `kit_version` 为 1.1.1,并且 Doctor 对 `--expected-kit 1.1.1` 输出 `OVERALL: PASS`。Plugin manifest 不属于这种 npm 安装方式的运行时前置条件,不得把不可访问的 Plugin 版本当作阻塞项。如果版本不一致、Skill 是在当前会话启动后才安装,或环境检查未通过,停止并报告前置条件不成立;不要在当前会话中替换 Skill 后继续生成工程。
12
23
 
13
24
  工作区与 clean-room 边界:
14
25
  - 将启动 Codex 时的当前目录定义为 `WORKSPACE_ROOT`。
@@ -16,7 +16,7 @@ The automatic branch never runs `sudo` or modifies a persistent shell profile. W
16
16
  When `<setup-root>/env.sh` exists, use it only as an activation prefix for the current command:
17
17
 
18
18
  ```bash
19
- bash -lc '. "<setup-root>/env.sh" && python3 "<skill-dir>/scripts/doctor.py" --expected-idf 5.5 --target esp32s3 --require-workspace'
19
+ bash -lc '. "<setup-root>/env.sh" && python3 "<skill-dir>/scripts/doctor.py" --expected-idf 5.5 --expected-kit 1.1.1 --target esp32s3 --require-workspace'
20
20
  ```
21
21
 
22
22
  The helper contains paths, not device or network credentials. Read `<setup-root>/config.json` when exact managed paths are needed; the environment helper does not authorize unrelated downloads, shell-profile changes, flashing, or credential writes.
@@ -26,6 +26,7 @@ Run the doctor before generation, build, flash, or monitor:
26
26
  ```bash
27
27
  python3 <skill-dir>/scripts/doctor.py \
28
28
  --expected-idf 5.5 \
29
+ --expected-kit 1.1.1 \
29
30
  --target esp32s3 \
30
31
  --require-workspace
31
32
  ```
@@ -34,7 +35,11 @@ Add `--project <generated-project>` after generation so the doctor can compare `
34
35
 
35
36
  ## ESP32 Device Kit
36
37
 
37
- The automatic setup downloads a versioned minimal Kit instead of cloning the ThingConnect server repository. Resolve the generation root in this order:
38
+ The automatic setup downloads a versioned minimal Kit instead of cloning the ThingConnect server repository. A managed Kit is ready only when its generator and SDK files exist and `manifest.json` declares the exact pinned `kit_version`. A stale environment or managed configuration that points at an older Kit is ignored in favor of the current versioned managed path. The setup configuration records the version read from that manifest; it never substitutes the desired version for the actual one.
39
+
40
+ The installed Skill has its own `<skill-dir>/VERSION` marker. Setup requires it to equal the npm package version and reports both values. Replacing a missing or mismatched marker requires the explicit `--install --force-skill` flow and a new Codex session.
41
+
42
+ For an explicitly selected legacy workspace, omit `--expected-kit`; otherwise resolve the generation root in this order:
38
43
 
39
44
  1. an explicit `--thing-connect-root <path>`;
40
45
  2. `TIRTC_THING_CONNECT_ROOT`;
@@ -27,6 +27,7 @@ GENERATOR_RELATIVE_PATH = Path("device-sim/scripts/create_esp32_project.py")
27
27
  DEFAULT_SDK_RELATIVE_PATH = Path(
28
28
  "device-sim/sdk/espressif-esp32s3/2.3.0"
29
29
  )
30
+ DEVICE_KIT_MANIFEST = "manifest.json"
30
31
 
31
32
 
32
33
  def check(name: str, status: str, detail: str, required: bool = True) -> dict[str, Any]:
@@ -166,6 +167,43 @@ def normalize_thing_connect_root(candidate: Path) -> Path | None:
166
167
  return None
167
168
 
168
169
 
170
+ def inspect_device_kit(root: Path | None) -> dict[str, str | bool | None]:
171
+ """Read the managed Kit identity without rejecting legacy workspaces."""
172
+ if root is None:
173
+ return {
174
+ "manifest_present": False,
175
+ "version": None,
176
+ "error": "Device Kit root is unresolved",
177
+ }
178
+ manifest_path = root / DEVICE_KIT_MANIFEST
179
+ if not manifest_path.is_file():
180
+ return {
181
+ "manifest_present": False,
182
+ "version": None,
183
+ "error": None,
184
+ }
185
+ try:
186
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
187
+ except (OSError, json.JSONDecodeError) as exc:
188
+ return {
189
+ "manifest_present": True,
190
+ "version": None,
191
+ "error": f"invalid {manifest_path}: {exc}",
192
+ }
193
+ version = payload.get("kit_version") if isinstance(payload, dict) else None
194
+ if not isinstance(version, str) or not version.strip():
195
+ return {
196
+ "manifest_present": True,
197
+ "version": None,
198
+ "error": f"{manifest_path} does not declare kit_version",
199
+ }
200
+ return {
201
+ "manifest_present": True,
202
+ "version": version.strip(),
203
+ "error": None,
204
+ }
205
+
206
+
169
207
  def find_thing_connect_root(
170
208
  explicit: Path | None,
171
209
  project: Path | None,
@@ -273,27 +311,51 @@ def diagnose(args: argparse.Namespace) -> dict[str, Any]:
273
311
  args.thing_connect_root,
274
312
  args.project,
275
313
  )
276
- workspace_status = "PASS" if thing_connect_root else (
277
- "FAIL" if args.require_workspace else "WARN"
278
- )
314
+ kit_identity = inspect_device_kit(thing_connect_root)
315
+ kit_error = kit_identity["error"]
316
+ actual_kit = kit_identity["version"]
317
+ if thing_connect_root is None:
318
+ workspace_status = "FAIL" if args.require_workspace or args.expected_kit else "WARN"
319
+ workspace_detail = (
320
+ f"not found; pass --thing-connect-root or set {THING_CONNECT_ENV}"
321
+ )
322
+ elif args.expected_kit and not kit_identity["manifest_present"]:
323
+ workspace_status = "FAIL"
324
+ workspace_detail = (
325
+ f"{thing_connect_root} ({thing_connect_source}) is an unversioned legacy "
326
+ f"workspace; expected managed Device Kit {args.expected_kit}"
327
+ )
328
+ elif kit_error:
329
+ workspace_status = "FAIL"
330
+ workspace_detail = str(kit_error)
331
+ elif args.expected_kit and actual_kit != args.expected_kit:
332
+ workspace_status = "FAIL"
333
+ workspace_detail = (
334
+ f"version {actual_kit or 'missing'} at {thing_connect_root} "
335
+ f"({thing_connect_source}); expected {args.expected_kit}"
336
+ )
337
+ else:
338
+ workspace_status = "PASS"
339
+ version_detail = (
340
+ f"version {actual_kit}"
341
+ if actual_kit
342
+ else "unversioned legacy workspace"
343
+ )
344
+ workspace_detail = (
345
+ f"{version_detail} at {thing_connect_root} ({thing_connect_source})"
346
+ )
279
347
  checks.append(
280
348
  check(
281
349
  "ESP32 Device Kit",
282
350
  workspace_status,
283
- (
284
- f"{thing_connect_root} ({thing_connect_source})"
285
- if thing_connect_root
286
- else (
287
- f"not found; pass --thing-connect-root or set {THING_CONNECT_ENV}"
288
- )
289
- ),
290
- required=args.require_workspace,
351
+ workspace_detail,
352
+ required=args.require_workspace or bool(args.expected_kit),
291
353
  )
292
354
  )
293
- if args.require_workspace and thing_connect_root is None:
355
+ if (args.require_workspace or args.expected_kit) and workspace_status == "FAIL":
294
356
  next_actions.append(
295
- "Run setup esp32 --install or pass an existing Device Kit path with "
296
- "--thing-connect-root."
357
+ "Run setup esp32 --install to select the pinned managed Device Kit, or "
358
+ "pass a matching Kit path with --thing-connect-root."
297
359
  )
298
360
 
299
361
  sdk_dir, sdk_source = resolve_sdk_dir(
@@ -390,6 +452,8 @@ def diagnose(args: argparse.Namespace) -> dict[str, Any]:
390
452
  return {
391
453
  "overall": overall,
392
454
  "expected_idf": args.expected_idf,
455
+ "expected_kit": args.expected_kit,
456
+ "device_kit_version": actual_kit,
393
457
  "target": args.target,
394
458
  "thing_connect_root": (
395
459
  str(thing_connect_root) if thing_connect_root is not None else None
@@ -414,6 +478,10 @@ def parse_args() -> argparse.Namespace:
414
478
  description="Check ESP-IDF, target tools, TiRTC SDK, project contract, and serial access."
415
479
  )
416
480
  parser.add_argument("--expected-idf", default="5.5")
481
+ parser.add_argument(
482
+ "--expected-kit",
483
+ help="require an exact managed ESP32 Device Kit manifest version",
484
+ )
417
485
  parser.add_argument("--target", default="esp32s3")
418
486
  parser.add_argument("--idf-py", type=Path)
419
487
  parser.add_argument("--thing-connect-root", type=Path)