@typecad/framework-zephyr 1.0.0-alpha.16 → 1.0.0-alpha.18

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.
Files changed (132) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/boardgen.d.ts +1 -9
  6. package/dist/boardgen.js +228 -45
  7. package/dist/chips/types.d.ts +1 -1
  8. package/dist/debug-codegen.js +1 -1
  9. package/dist/display/touch-adapter.js +1 -1
  10. package/dist/display/ui-adapter.js +549 -549
  11. package/dist/doctor.js +4 -4
  12. package/dist/dt-config/custom-board.js +2 -2
  13. package/dist/dt-config/kconfig.js +18 -12
  14. package/dist/dt-config/overlay.d.ts +2 -2
  15. package/dist/dt-config/overlay.js +2 -2
  16. package/dist/framework.manifest.d.ts +3 -3
  17. package/dist/framework.manifest.js +10 -7
  18. package/dist/index.js +5 -5
  19. package/dist/licenses.d.ts +2 -2
  20. package/dist/licenses.js +8 -8
  21. package/dist/lowering/fs.js +1 -1
  22. package/dist/lowering/gpio.js +0 -32
  23. package/dist/lowering/http.js +264 -32
  24. package/dist/lowering/i2c.js +0 -8
  25. package/dist/lowering/interrupts.js +6 -0
  26. package/dist/lowering/mqtt.js +110 -8
  27. package/dist/lowering/usb.js +11 -11
  28. package/dist/lowering/wdt.js +2 -29
  29. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  30. package/dist/sdk/board-catalog-sync.js +4 -10
  31. package/dist/strategy.d.ts +22 -44
  32. package/dist/strategy.js +152 -155
  33. package/dist/tmp-probe.d.ts +2 -0
  34. package/dist/tmp-probe.js +9 -0
  35. package/dist/toolchain/debug-config.d.ts +50 -90
  36. package/dist/toolchain/debug-config.js +239 -510
  37. package/dist/toolchain/env-check.d.ts +1 -3
  38. package/dist/toolchain/env-check.js +2 -7
  39. package/dist/toolchain/index.d.ts +16 -2
  40. package/dist/toolchain/index.js +383 -35
  41. package/dist/toolchain/runners.d.ts +16 -0
  42. package/dist/toolchain/runners.js +75 -0
  43. package/dist/toolchain/scaffold.d.ts +1 -1
  44. package/dist/toolchain/scaffold.js +12 -12
  45. package/dist/toolchain/west-discover.d.ts +6 -0
  46. package/dist/toolchain/west-discover.js +36 -13
  47. package/dist/toolchain/west-spawn.js +8 -2
  48. package/installer/README.md +328 -328
  49. package/installer/install.sh +2 -2
  50. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  51. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  52. package/installer/templates/project/.vscode/settings.json +1 -1
  53. package/installer/templates/project/README.md +2 -2
  54. package/package.json +5 -5
  55. package/src/as-built.ts +206 -206
  56. package/src/boardgen.ts +214 -48
  57. package/src/chips/types.ts +567 -567
  58. package/src/display/touch-adapter.ts +204 -204
  59. package/src/display/ui-adapter.ts +781 -781
  60. package/src/doctor.ts +4 -4
  61. package/src/dt-config/custom-board.ts +2 -2
  62. package/src/dt-config/kconfig.ts +18 -12
  63. package/src/dt-config/overlay.ts +1058 -1058
  64. package/src/framework.manifest.ts +538 -535
  65. package/src/index.ts +5 -5
  66. package/src/licenses.ts +425 -425
  67. package/src/lowering/fs.ts +135 -135
  68. package/src/lowering/gpio.ts +0 -33
  69. package/src/lowering/http.ts +264 -32
  70. package/src/lowering/i2c.ts +0 -9
  71. package/src/lowering/interrupts.ts +6 -0
  72. package/src/lowering/mqtt.ts +109 -8
  73. package/src/lowering/usb.ts +221 -221
  74. package/src/lowering/wdt.ts +2 -25
  75. package/src/sdk/board-catalog-sync.ts +4 -25
  76. package/src/strategy.ts +2265 -2309
  77. package/src/toolchain/debug-config.ts +262 -522
  78. package/src/toolchain/env-check.ts +279 -285
  79. package/src/toolchain/index.ts +1703 -1359
  80. package/src/toolchain/runners.ts +80 -0
  81. package/src/toolchain/scaffold.ts +296 -296
  82. package/src/toolchain/west-discover.ts +35 -13
  83. package/src/toolchain/west-spawn.ts +174 -168
  84. package/dist/async/timer-polyfill.d.ts +0 -10
  85. package/dist/async/timer-polyfill.js +0 -95
  86. package/dist/chips/board-overrides.d.ts +0 -7
  87. package/dist/chips/board-overrides.js +0 -11
  88. package/dist/chips/esp32.d.ts +0 -2
  89. package/dist/chips/esp32.js +0 -71
  90. package/dist/chips/esp32s3.d.ts +0 -2
  91. package/dist/chips/esp32s3.js +0 -103
  92. package/dist/chips/soc/.d.ts +0 -2
  93. package/dist/chips/soc/.js +0 -129
  94. package/dist/chips/soc/esp32.d.ts +0 -2
  95. package/dist/chips/soc/esp32.js +0 -120
  96. package/dist/chips/soc/esp32c3.d.ts +0 -2
  97. package/dist/chips/soc/esp32c3.js +0 -90
  98. package/dist/chips/soc/esp32c6.d.ts +0 -2
  99. package/dist/chips/soc/esp32c6.js +0 -109
  100. package/dist/chips/soc/esp32s3.d.ts +0 -2
  101. package/dist/chips/soc/esp32s3.js +0 -189
  102. package/dist/chips/soc/index.d.ts +0 -2
  103. package/dist/chips/soc/index.js +0 -23
  104. package/dist/chips/soc/nrf52840.d.ts +0 -2
  105. package/dist/chips/soc/nrf52840.js +0 -130
  106. package/dist/chips/soc/rp2040.d.ts +0 -2
  107. package/dist/chips/soc/rp2040.js +0 -141
  108. package/dist/chips/soc/rp2350a.d.ts +0 -2
  109. package/dist/chips/soc/rp2350a.js +0 -145
  110. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  111. package/dist/chips/soc/samd21g18a.js +0 -143
  112. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  113. package/dist/chips/soc/stm32f411xe.js +0 -251
  114. package/dist/chips/xiao-ble.d.ts +0 -2
  115. package/dist/chips/xiao-ble.js +0 -100
  116. package/dist/lowering/power.d.ts +0 -9
  117. package/dist/lowering/power.js +0 -60
  118. package/dist/lowering/pulse.d.ts +0 -7
  119. package/dist/lowering/pulse.js +0 -51
  120. package/dist/lowering/tone.d.ts +0 -10
  121. package/dist/lowering/tone.js +0 -63
  122. package/dist/lowering/worker-backing.d.ts +0 -14
  123. package/dist/lowering/worker-backing.js +0 -79
  124. package/dist/lowering/worker.d.ts +0 -6
  125. package/dist/lowering/worker.js +0 -14
  126. package/dist/sdk/board-data.generated.d.ts +0 -2
  127. package/dist/sdk/board-data.generated.js +0 -4
  128. package/dist/sdk/catalog-walker.d.ts +0 -90
  129. package/dist/sdk/catalog-walker.js +0 -682
  130. package/dist/sdk/dts-reader.d.ts +0 -83
  131. package/dist/sdk/dts-reader.js +0 -596
  132. package/src/debug-codegen.ts +0 -207
@@ -1,285 +1,279 @@
1
- // ---------------------------------------------------------------------------
2
- // Zephyr environment check — the shared detection behind `cuttlefish doctor`.
3
- //
4
- // Zephyr environment check: gather the impure
5
- // environment facts once (west presence + version, Zephyr version, board
6
- // existence), then reduce them to a structured result the doctor (and, later,
7
- // the build/test gates) can present uniformly. The check is side-effect-free
8
- // and never throws — it never installs or mutates anything.
9
- //
10
- // Two checks (mirroring the doctor contract):
11
- // 1. west (the Zephyr build tool) is discoverable + responsive — the direct
12
- // toolchain presence. discoverWest() already confirms
13
- // responsiveness via `west --version`; we additionally capture the version
14
- // string to report it.
15
- // 2. the configured board target exists in the Zephyr checkout
16
- // ($ZEPHYR_BASE/boards/) — the analog of "the required core is installed".
17
- //
18
- // The existing compat-range check (compat.ts) is folded in as a third check so
19
- // the doctor reports everything through one entry point.
20
- // ---------------------------------------------------------------------------
21
-
22
- import { spawnSync } from 'node:child_process';
23
- import { existsSync, readdirSync } from 'node:fs';
24
- import { join } from 'node:path';
25
-
26
- import { type WestInstall, discoverWest, resetWestDiscoveryCache } from './west-discover.js';
27
- import { westSpawn } from './west-spawn.js';
28
- import {
29
- detectZephyrVersion,
30
- checkZephyrCompat,
31
- resolveBoardTarget,
32
- type CompatStatus,
33
- } from './compat.js';
34
-
35
- // ---- probe data (impure facts, injectable for tests) -----------------------
36
-
37
- /**
38
- * Raw west facts gathered from discovery + a `west --version` probe. Mirrors
39
- * probe data: `westFound` is true when a usable west install was
40
- * discovered (discovery itself probes responsiveness).
41
- */
42
- export interface WestProbeData {
43
- /** A usable west install was discovered. */
44
- westFound: boolean;
45
- /** west version string if the `--version` probe parsed one, e.g. "1.3.0". */
46
- westVersion: string | undefined;
47
- /** Which discovery strategy found west, for surfacing to the user. */
48
- source: WestInstall['source'] | undefined;
49
- /** Effective ZEPHYR_BASE (env var, else a base discovery surfaced). */
50
- zephyrBase: string | undefined;
51
- }
52
-
53
- /**
54
- * Test-injection seam for checkZephyrEnv. Mirrors the
55
- * fakeProbe option so tests never spawn a real west/python.
56
- */
57
- export interface CheckZephyrEnvOptions {
58
- /** FOR TESTS ONLY: skip the real probe and use this data directly. */
59
- fakeWestProbe?: WestProbeData;
60
- /** FOR TESTS ONLY: override the board-existence lookup. */
61
- fakeBoardExists?: (boardId: string, zephyrBase: string | undefined) => boolean | undefined;
62
- }
63
-
64
- // ---- result types ------------------------------------------------------------
65
-
66
- export interface ZephyrEnvCheck {
67
- /** west (the Zephyr build tool) was discovered and responsive. */
68
- westFound: boolean;
69
- /** west version string if known, e.g. "1.3.0". */
70
- westVersion: string | undefined;
71
- /** Discovery strategy that found west, for display. */
72
- westSource: WestInstall['source'] | undefined;
73
- /** Effective ZEPHYR_BASE (env var, else a discovered base). */
74
- zephyrBase: string | undefined;
75
- /** Detected Zephyr RTOS version from $ZEPHYR_BASE/VERSION, if readable. */
76
- zephyrVersion: string | undefined;
77
- /** Declared supported range (manifest.compat.zephyr), if any. */
78
- compatRange: string | undefined;
79
- /** Result of the compat-range check against the detected version. */
80
- compatStatus: CompatStatus;
81
- /** Raw board target from cuttlefish.config.ts, if configured. */
82
- buildTarget: string | undefined;
83
- /** buildTarget normalized for the installed Zephyr version (may equal it). */
84
- resolvedBoardTarget: string | undefined;
85
- /** Does the resolved board exist in the checkout? undefined = undetermined. */
86
- boardTargetSupported: boolean | undefined;
87
- }
88
-
89
- export type ZephyrEnvOk = { ok: true; check: ZephyrEnvCheck };
90
-
91
- export type ZephyrEnvFailure = {
92
- ok: false;
93
- reason: 'west-not-found' | 'zephyr-out-of-range' | 'board-not-supported';
94
- check: ZephyrEnvCheck;
95
- /** Human-readable lines ready to print. */
96
- messages: string[];
97
- /** Exact remediation hint, when applicable. */
98
- fixCommand: string | undefined;
99
- };
100
-
101
- export type ZephyrEnvResult = ZephyrEnvOk | ZephyrEnvFailure;
102
-
103
- // ---- west probe (impure; isolated + cached + overridable) ------------------
104
-
105
- let cachedProbe: WestProbeData | undefined;
106
-
107
- /** Clear the west-probe cache (for tests). Also resets discovery cache. */
108
- export function resetWestProbeCacheForTest(): void {
109
- cachedProbe = undefined;
110
- resetWestDiscoveryCache();
111
- }
112
-
113
- /**
114
- * Gather west facts: discover a usable install, then run `west --version`
115
- * through it to capture the version. Memoized for the process lifetime (west
116
- * installs don't move). Never throws — returns westFound:false on any failure.
117
- */
118
- export function probeWestEnv(): WestProbeData {
119
- if (cachedProbe) return cachedProbe;
120
-
121
- const install = discoverWest();
122
- const envBase = process.env.ZEPHYR_BASE || undefined;
123
- if (!install) {
124
- const data: WestProbeData = {
125
- westFound: false,
126
- westVersion: undefined,
127
- source: undefined,
128
- zephyrBase: envBase,
129
- };
130
- cachedProbe = data;
131
- return data;
132
- }
133
-
134
- // Run `west --version` through the discovered install to capture the version.
135
- // discoverWest() already confirmed responsiveness, so a parse failure here is
136
- // not "unresponsive" — it just means we couldn't read a version token.
137
- let westVersion: string | undefined;
138
- try {
139
- const inv = westSpawn(['--version'], {
140
- encoding: 'utf8',
141
- timeout: 15_000,
142
- windowsHide: true,
143
- });
144
- const r = spawnSync(inv.command, inv.args, inv.options);
145
- if (r.status === 0) {
146
- // inv.options is a generic SpawnSyncOptions (no encoding literal), so
147
- // coerce stdout to a string before matching.
148
- const out = typeof r.stdout === 'string' ? r.stdout : '';
149
- const m = out.match(/v?(\d+\.\d+\.\d+)/);
150
- westVersion = m ? m[1] : undefined;
151
- }
152
- } catch {
153
- // westSpawn throws only when discovery fails — but discovery already
154
- // succeeded (install is non-null). Defensive: treat as no version read.
155
- westVersion = undefined;
156
- }
157
-
158
- const data: WestProbeData = {
159
- westFound: true,
160
- westVersion,
161
- source: install.source,
162
- zephyrBase: envBase ?? install.zephyrBase,
163
- };
164
- cachedProbe = data;
165
- return data;
166
- }
167
-
168
- // ---- board existence (pure-ish fs probe) -----------------------------------
169
-
170
- /**
171
- * Does `boardId` exist as a board directory in the Zephyr checkout? Checks the
172
- * HWMv2 vendor layout used by Zephyr 4.x: $ZEPHYR_BASE/boards/<vendor>/<boardId>.
173
- * Returns true/false when determinable; undefined when the base is unknown or
174
- * the boards/ tree can't be read (so callers never fail on an inconclusive
175
- * lookup — they just skip the board check).
176
- */
177
- export function boardExistsInCheckout(
178
- boardId: string,
179
- zephyrBase: string | undefined,
180
- ): boolean | undefined {
181
- if (!zephyrBase) return undefined;
182
- const boards = join(zephyrBase, 'boards');
183
- try {
184
- const entries = readdirSync(boards, { withFileTypes: true });
185
- for (const entry of entries) {
186
- if (entry.isDirectory() && existsSync(join(boards, entry.name, boardId))) {
187
- return true;
188
- }
189
- }
190
- return false;
191
- } catch {
192
- return undefined;
193
- }
194
- }
195
-
196
- // ---- main entry point -------------------------------------------------------
197
-
198
- /**
199
- * Verify the environment can build for `buildTarget`. Cheap and
200
- * side-effect-free: discovers west, reads the Zephyr version, checks the compat
201
- * range, and — when a target is configured — verifies the board exists in the
202
- * checkout. Reports what (if anything) is wrong.
203
- *
204
- * - If `buildTarget` is undefined/empty, the board check is skipped (not a
205
- * failure), mirroring the no-build-target path.
206
- * - Never installs anything. Never mutates the user environment.
207
- * - Never throws — always returns a result. Callers decide how to react.
208
- *
209
- * `options` is for-test only (injects fake probe data / board lookup).
210
- */
211
- export function checkZephyrEnv(
212
- buildTarget?: string,
213
- options?: CheckZephyrEnvOptions,
214
- ): ZephyrEnvResult {
215
- const probe = options?.fakeWestProbe ?? probeWestEnv();
216
- const boardLookup = options?.fakeBoardExists ?? boardExistsInCheckout;
217
-
218
- const zephyrVersion = detectZephyrVersion();
219
- const compat = checkZephyrCompat(zephyrVersion);
220
- const resolvedBoardTarget = buildTarget ? resolveBoardTarget(buildTarget, zephyrVersion) : undefined;
221
- const boardId = resolvedBoardTarget ? resolvedBoardTarget.split('/')[0]! : undefined;
222
- const boardTargetSupported =
223
- boardId !== undefined ? boardLookup(boardId, probe.zephyrBase) : undefined;
224
-
225
- const check: ZephyrEnvCheck = {
226
- westFound: probe.westFound,
227
- westVersion: probe.westVersion,
228
- westSource: probe.source,
229
- zephyrBase: probe.zephyrBase,
230
- zephyrVersion,
231
- compatRange: compat.range,
232
- compatStatus: compat.status,
233
- buildTarget,
234
- resolvedBoardTarget,
235
- boardTargetSupported,
236
- };
237
-
238
- // 1. west (the build tool) missing entirely — nothing else can run.
239
- if (!probe.westFound) {
240
- return {
241
- ok: false,
242
- reason: 'west-not-found',
243
- check,
244
- messages: [
245
- "west (the Zephyr build tool) was not found.",
246
- " Run the typeCAD Zephyr installer, activate an existing Zephyr venv,",
247
- " set ZEPHYR_BASE to a Zephyr SDK root, or `pip install west`.",
248
- ],
249
- fixCommand: undefined,
250
- };
251
- }
252
-
253
- // 2. west healthy but the Zephyr RTOS is outside the supported range.
254
- if (compat.status === 'out-of-range') {
255
- return {
256
- ok: false,
257
- reason: 'zephyr-out-of-range',
258
- check,
259
- messages: [
260
- `Zephyr ${zephyrVersion} is outside the supported range (${compat.range}) for @typecad/framework-zephyr.`,
261
- " Set ZEPHYR_BASE to a compatible Zephyr checkout, or or run the bundled Zephyr installer (npx --package @typecad/framework-zephyr zephyr-installer).",
262
- ],
263
- fixCommand: undefined,
264
- };
265
- }
266
-
267
- // 3. west + version OK — only check the board when a target is configured and
268
- // the lookup was able to answer. A missing/absent target is not a board
269
- // problem; an inconclusive lookup (no base) is reported as a skip, not a fail.
270
- if (buildTarget && boardTargetSupported === false) {
271
- return {
272
- ok: false,
273
- reason: 'board-not-supported',
274
- check,
275
- messages: [
276
- `Board target '${resolvedBoardTarget}' was not found in this Zephyr checkout` +
277
- (probe.zephyrBase ? ` (${join(probe.zephyrBase, 'boards')}).` : '.'),
278
- " Check the board id, or run `west boards` to list boards in this checkout.",
279
- ],
280
- fixCommand: 'west boards',
281
- };
282
- }
283
-
284
- return { ok: true, check };
285
- }
1
+ // ---------------------------------------------------------------------------
2
+ // Zephyr environment check — the shared detection behind `typecad-hal doctor`.
3
+ //
4
+ // Zephyr environment check: gather the impure
5
+ // environment facts once (west presence + version, Zephyr version, board
6
+ // existence), then reduce them to a structured result the doctor (and, later,
7
+ // the build/test gates) can present uniformly. The check is side-effect-free
8
+ // and never throws — it never installs or mutates anything.
9
+ //
10
+ // Two checks (mirroring the doctor contract):
11
+ // 1. west (the Zephyr build tool) is discoverable + responsive — the direct
12
+ // toolchain presence. discoverWest() already confirms
13
+ // responsiveness via `west --version`; we additionally capture the version
14
+ // string to report it.
15
+ // 2. the configured board target exists in the Zephyr checkout
16
+ // ($ZEPHYR_BASE/boards/) — the analog of "the required core is installed".
17
+ //
18
+ // The existing compat-range check (compat.ts) is folded in as a third check so
19
+ // the doctor reports everything through one entry point.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+ import { existsSync, readdirSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+
26
+ import { type WestInstall, discoverWest } from './west-discover.js';
27
+ import { westSpawn } from './west-spawn.js';
28
+ import {
29
+ detectZephyrVersion,
30
+ checkZephyrCompat,
31
+ resolveBoardTarget,
32
+ type CompatStatus,
33
+ } from './compat.js';
34
+
35
+ // ---- probe data (impure facts, injectable for tests) -----------------------
36
+
37
+ /**
38
+ * Raw west facts gathered from discovery + a `west --version` probe. Mirrors
39
+ * probe data: `westFound` is true when a usable west install was
40
+ * discovered (discovery itself probes responsiveness).
41
+ */
42
+ export interface WestProbeData {
43
+ /** A usable west install was discovered. */
44
+ westFound: boolean;
45
+ /** west version string if the `--version` probe parsed one, e.g. "1.3.0". */
46
+ westVersion: string | undefined;
47
+ /** Which discovery strategy found west, for surfacing to the user. */
48
+ source: WestInstall['source'] | undefined;
49
+ /** Effective ZEPHYR_BASE (env var, else a base discovery surfaced). */
50
+ zephyrBase: string | undefined;
51
+ }
52
+
53
+ /**
54
+ * Test-injection seam for checkZephyrEnv. Mirrors the
55
+ * fakeProbe option so tests never spawn a real west/python.
56
+ */
57
+ export interface CheckZephyrEnvOptions {
58
+ /** FOR TESTS ONLY: skip the real probe and use this data directly. */
59
+ fakeWestProbe?: WestProbeData;
60
+ /** FOR TESTS ONLY: override the board-existence lookup. */
61
+ fakeBoardExists?: (boardId: string, zephyrBase: string | undefined) => boolean | undefined;
62
+ }
63
+
64
+ // ---- result types ------------------------------------------------------------
65
+
66
+ export interface ZephyrEnvCheck {
67
+ /** west (the Zephyr build tool) was discovered and responsive. */
68
+ westFound: boolean;
69
+ /** west version string if known, e.g. "1.3.0". */
70
+ westVersion: string | undefined;
71
+ /** Discovery strategy that found west, for display. */
72
+ westSource: WestInstall['source'] | undefined;
73
+ /** Effective ZEPHYR_BASE (env var, else a discovered base). */
74
+ zephyrBase: string | undefined;
75
+ /** Detected Zephyr RTOS version from $ZEPHYR_BASE/VERSION, if readable. */
76
+ zephyrVersion: string | undefined;
77
+ /** Declared supported range (manifest.compat.zephyr), if any. */
78
+ compatRange: string | undefined;
79
+ /** Result of the compat-range check against the detected version. */
80
+ compatStatus: CompatStatus;
81
+ /** Raw board target from typecad-hal.config.ts, if configured. */
82
+ buildTarget: string | undefined;
83
+ /** buildTarget normalized for the installed Zephyr version (may equal it). */
84
+ resolvedBoardTarget: string | undefined;
85
+ /** Does the resolved board exist in the checkout? undefined = undetermined. */
86
+ boardTargetSupported: boolean | undefined;
87
+ }
88
+
89
+ export type ZephyrEnvOk = { ok: true; check: ZephyrEnvCheck };
90
+
91
+ export type ZephyrEnvFailure = {
92
+ ok: false;
93
+ reason: 'west-not-found' | 'zephyr-out-of-range' | 'board-not-supported';
94
+ check: ZephyrEnvCheck;
95
+ /** Human-readable lines ready to print. */
96
+ messages: string[];
97
+ /** Exact remediation hint, when applicable. */
98
+ fixCommand: string | undefined;
99
+ };
100
+
101
+ export type ZephyrEnvResult = ZephyrEnvOk | ZephyrEnvFailure;
102
+
103
+ // ---- west probe (impure; isolated + cached + overridable) ------------------
104
+
105
+ let cachedProbe: WestProbeData | undefined;
106
+
107
+ /**
108
+ * Gather west facts: discover a usable install, then run `west --version`
109
+ * through it to capture the version. Memoized for the process lifetime (west
110
+ * installs don't move). Never throws — returns westFound:false on any failure.
111
+ */
112
+ export function probeWestEnv(): WestProbeData {
113
+ if (cachedProbe) return cachedProbe;
114
+
115
+ const install = discoverWest();
116
+ const envBase = process.env.ZEPHYR_BASE || undefined;
117
+ if (!install) {
118
+ const data: WestProbeData = {
119
+ westFound: false,
120
+ westVersion: undefined,
121
+ source: undefined,
122
+ zephyrBase: envBase,
123
+ };
124
+ cachedProbe = data;
125
+ return data;
126
+ }
127
+
128
+ // Run `west --version` through the discovered install to capture the version.
129
+ // discoverWest() already confirmed responsiveness, so a parse failure here is
130
+ // not "unresponsive" — it just means we couldn't read a version token.
131
+ let westVersion: string | undefined;
132
+ try {
133
+ const inv = westSpawn(['--version'], {
134
+ encoding: 'utf8',
135
+ timeout: 15_000,
136
+ windowsHide: true,
137
+ });
138
+ const r = spawnSync(inv.command, inv.args, inv.options);
139
+ if (r.status === 0) {
140
+ // inv.options is a generic SpawnSyncOptions (no encoding literal), so
141
+ // coerce stdout to a string before matching.
142
+ const out = typeof r.stdout === 'string' ? r.stdout : '';
143
+ const m = out.match(/v?(\d+\.\d+\.\d+)/);
144
+ westVersion = m ? m[1] : undefined;
145
+ }
146
+ } catch {
147
+ // westSpawn throws only when discovery fails — but discovery already
148
+ // succeeded (install is non-null). Defensive: treat as no version read.
149
+ westVersion = undefined;
150
+ }
151
+
152
+ const data: WestProbeData = {
153
+ westFound: true,
154
+ westVersion,
155
+ source: install.source,
156
+ zephyrBase: envBase ?? install.zephyrBase,
157
+ };
158
+ cachedProbe = data;
159
+ return data;
160
+ }
161
+
162
+ // ---- board existence (pure-ish fs probe) -----------------------------------
163
+
164
+ /**
165
+ * Does `boardId` exist as a board directory in the Zephyr checkout? Checks the
166
+ * HWMv2 vendor layout used by Zephyr 4.x: $ZEPHYR_BASE/boards/<vendor>/<boardId>.
167
+ * Returns true/false when determinable; undefined when the base is unknown or
168
+ * the boards/ tree can't be read (so callers never fail on an inconclusive
169
+ * lookup — they just skip the board check).
170
+ */
171
+ export function boardExistsInCheckout(
172
+ boardId: string,
173
+ zephyrBase: string | undefined,
174
+ ): boolean | undefined {
175
+ if (!zephyrBase) return undefined;
176
+ const boards = join(zephyrBase, 'boards');
177
+ try {
178
+ const entries = readdirSync(boards, { withFileTypes: true });
179
+ for (const entry of entries) {
180
+ if (entry.isDirectory() && existsSync(join(boards, entry.name, boardId))) {
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ } catch {
186
+ return undefined;
187
+ }
188
+ }
189
+
190
+ // ---- main entry point -------------------------------------------------------
191
+
192
+ /**
193
+ * Verify the environment can build for `buildTarget`. Cheap and
194
+ * side-effect-free: discovers west, reads the Zephyr version, checks the compat
195
+ * range, and — when a target is configured — verifies the board exists in the
196
+ * checkout. Reports what (if anything) is wrong.
197
+ *
198
+ * - If `buildTarget` is undefined/empty, the board check is skipped (not a
199
+ * failure), mirroring the no-build-target path.
200
+ * - Never installs anything. Never mutates the user environment.
201
+ * - Never throws — always returns a result. Callers decide how to react.
202
+ *
203
+ * `options` is for-test only (injects fake probe data / board lookup).
204
+ */
205
+ export function checkZephyrEnv(
206
+ buildTarget?: string,
207
+ options?: CheckZephyrEnvOptions,
208
+ ): ZephyrEnvResult {
209
+ const probe = options?.fakeWestProbe ?? probeWestEnv();
210
+ const boardLookup = options?.fakeBoardExists ?? boardExistsInCheckout;
211
+
212
+ const zephyrVersion = detectZephyrVersion();
213
+ const compat = checkZephyrCompat(zephyrVersion);
214
+ const resolvedBoardTarget = buildTarget ? resolveBoardTarget(buildTarget, zephyrVersion) : undefined;
215
+ const boardId = resolvedBoardTarget ? resolvedBoardTarget.split('/')[0]! : undefined;
216
+ const boardTargetSupported =
217
+ boardId !== undefined ? boardLookup(boardId, probe.zephyrBase) : undefined;
218
+
219
+ const check: ZephyrEnvCheck = {
220
+ westFound: probe.westFound,
221
+ westVersion: probe.westVersion,
222
+ westSource: probe.source,
223
+ zephyrBase: probe.zephyrBase,
224
+ zephyrVersion,
225
+ compatRange: compat.range,
226
+ compatStatus: compat.status,
227
+ buildTarget,
228
+ resolvedBoardTarget,
229
+ boardTargetSupported,
230
+ };
231
+
232
+ // 1. west (the build tool) missing entirely — nothing else can run.
233
+ if (!probe.westFound) {
234
+ return {
235
+ ok: false,
236
+ reason: 'west-not-found',
237
+ check,
238
+ messages: [
239
+ "west (the Zephyr build tool) was not found.",
240
+ " Run the typeCAD Zephyr installer, activate an existing Zephyr venv,",
241
+ " set ZEPHYR_BASE to a Zephyr SDK root, or `pip install west`.",
242
+ ],
243
+ fixCommand: undefined,
244
+ };
245
+ }
246
+
247
+ // 2. west healthy but the Zephyr RTOS is outside the supported range.
248
+ if (compat.status === 'out-of-range') {
249
+ return {
250
+ ok: false,
251
+ reason: 'zephyr-out-of-range',
252
+ check,
253
+ messages: [
254
+ `Zephyr ${zephyrVersion} is outside the supported range (${compat.range}) for @typecad/framework-zephyr.`,
255
+ " Set ZEPHYR_BASE to a compatible Zephyr checkout, or or run the bundled Zephyr installer (npx --package @typecad/framework-zephyr zephyr-installer).",
256
+ ],
257
+ fixCommand: undefined,
258
+ };
259
+ }
260
+
261
+ // 3. west + version OK — only check the board when a target is configured and
262
+ // the lookup was able to answer. A missing/absent target is not a board
263
+ // problem; an inconclusive lookup (no base) is reported as a skip, not a fail.
264
+ if (buildTarget && boardTargetSupported === false) {
265
+ return {
266
+ ok: false,
267
+ reason: 'board-not-supported',
268
+ check,
269
+ messages: [
270
+ `Board target '${resolvedBoardTarget}' was not found in this Zephyr checkout` +
271
+ (probe.zephyrBase ? ` (${join(probe.zephyrBase, 'boards')}).` : '.'),
272
+ " Check the board id, or run `west boards` to list boards in this checkout.",
273
+ ],
274
+ fixCommand: 'west boards',
275
+ };
276
+ }
277
+
278
+ return { ok: true, check };
279
+ }