@typecad/framework-zephyr 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (191) hide show
  1. package/README.md +22 -35
  2. package/dist/as-built.d.ts +33 -0
  3. package/dist/as-built.js +188 -0
  4. package/dist/async/timer-polyfill.js +1 -1
  5. package/dist/boardgen.d.ts +128 -0
  6. package/dist/boardgen.js +1366 -0
  7. package/dist/chips/board-overrides.d.ts +7 -0
  8. package/dist/chips/board-overrides.js +11 -0
  9. package/dist/chips/controllers.d.ts +28 -8
  10. package/dist/chips/controllers.js +49 -12
  11. package/dist/chips/esp32s3.js +49 -0
  12. package/dist/chips/index.d.ts +6 -12
  13. package/dist/chips/index.js +18 -34
  14. package/dist/chips/resolve.d.ts +16 -2
  15. package/dist/chips/resolve.js +298 -15
  16. package/dist/chips/soc/.d.ts +2 -0
  17. package/dist/chips/soc/.js +129 -0
  18. package/dist/chips/soc/esp32.d.ts +2 -0
  19. package/dist/chips/soc/esp32.js +120 -0
  20. package/dist/chips/soc/esp32c3.d.ts +2 -0
  21. package/dist/chips/soc/esp32c3.js +90 -0
  22. package/dist/chips/soc/esp32c6.d.ts +2 -0
  23. package/dist/chips/soc/esp32c6.js +109 -0
  24. package/dist/chips/soc/esp32s3.d.ts +2 -0
  25. package/dist/chips/soc/esp32s3.js +189 -0
  26. package/dist/chips/soc/index.d.ts +2 -0
  27. package/dist/chips/soc/index.js +23 -0
  28. package/dist/chips/soc/nrf52840.d.ts +2 -0
  29. package/dist/chips/soc/nrf52840.js +130 -0
  30. package/dist/chips/soc/rp2040.d.ts +2 -0
  31. package/dist/chips/soc/rp2040.js +141 -0
  32. package/dist/chips/soc/rp2350a.d.ts +2 -0
  33. package/dist/chips/soc/rp2350a.js +145 -0
  34. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  35. package/dist/chips/soc/samd21g18a.js +143 -0
  36. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  37. package/dist/chips/soc/stm32f411xe.js +251 -0
  38. package/dist/chips/types.d.ts +382 -12
  39. package/dist/chips/xiao-ble.js +33 -0
  40. package/dist/display/profiles.js +1 -1
  41. package/dist/doctor.js +38 -4
  42. package/dist/dt-config/custom-board.d.ts +23 -0
  43. package/dist/dt-config/custom-board.js +227 -0
  44. package/dist/dt-config/kconfig.d.ts +83 -1
  45. package/dist/dt-config/kconfig.js +116 -27
  46. package/dist/dt-config/overlay.js +623 -18
  47. package/dist/framework.manifest.d.ts +20 -30
  48. package/dist/framework.manifest.js +124 -178
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.js +8 -2
  51. package/dist/lowering/adc.d.ts +26 -5
  52. package/dist/lowering/adc.js +137 -31
  53. package/dist/lowering/ble.js +31 -28
  54. package/dist/lowering/dac.js +9 -9
  55. package/dist/lowering/fs.js +18 -10
  56. package/dist/lowering/gpio.d.ts +2 -0
  57. package/dist/lowering/gpio.js +90 -10
  58. package/dist/lowering/http.js +39 -14
  59. package/dist/lowering/hwtimer.d.ts +4 -0
  60. package/dist/lowering/hwtimer.js +35 -10
  61. package/dist/lowering/i2c.js +46 -54
  62. package/dist/lowering/index.d.ts +5 -5
  63. package/dist/lowering/index.js +15 -16
  64. package/dist/lowering/interrupts.d.ts +22 -5
  65. package/dist/lowering/interrupts.js +104 -22
  66. package/dist/lowering/mqtt.js +12 -6
  67. package/dist/lowering/power.js +0 -4
  68. package/dist/lowering/preferences.js +49 -77
  69. package/dist/lowering/pulse.js +5 -36
  70. package/dist/lowering/pwm.d.ts +32 -3
  71. package/dist/lowering/pwm.js +105 -17
  72. package/dist/lowering/random.js +3 -3
  73. package/dist/lowering/sensor.d.ts +37 -0
  74. package/dist/lowering/sensor.js +119 -0
  75. package/dist/lowering/spi.d.ts +12 -0
  76. package/dist/lowering/spi.js +65 -40
  77. package/dist/lowering/thread.d.ts +14 -0
  78. package/dist/lowering/thread.js +55 -0
  79. package/dist/lowering/timing.js +20 -46
  80. package/dist/lowering/tone.js +18 -5
  81. package/dist/lowering/uart.d.ts +13 -0
  82. package/dist/lowering/uart.js +72 -51
  83. package/dist/lowering/usb.d.ts +34 -0
  84. package/dist/lowering/usb.js +206 -0
  85. package/dist/lowering/wdt.d.ts +1 -1
  86. package/dist/lowering/wdt.js +17 -7
  87. package/dist/lowering/wifi.d.ts +3 -2
  88. package/dist/lowering/wifi.js +120 -111
  89. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  90. package/dist/sdk/board-catalog-sync.js +41 -0
  91. package/dist/sdk/board-data.generated.d.ts +2 -0
  92. package/dist/sdk/board-data.generated.js +4 -0
  93. package/dist/sdk/catalog-walker.d.ts +90 -0
  94. package/dist/sdk/catalog-walker.js +682 -0
  95. package/dist/sdk/dts-reader.d.ts +83 -0
  96. package/dist/sdk/dts-reader.js +596 -0
  97. package/dist/strategy.d.ts +136 -17
  98. package/dist/strategy.js +881 -212
  99. package/dist/toolchain/bossac-touch.d.ts +18 -0
  100. package/dist/toolchain/bossac-touch.js +88 -0
  101. package/dist/toolchain/compat.d.ts +2 -2
  102. package/dist/toolchain/compat.js +8 -3
  103. package/dist/toolchain/debug-config.d.ts +21 -26
  104. package/dist/toolchain/debug-config.js +212 -72
  105. package/dist/toolchain/env-check.d.ts +4 -4
  106. package/dist/toolchain/env-check.js +5 -5
  107. package/dist/toolchain/index.d.ts +66 -1
  108. package/dist/toolchain/index.js +713 -58
  109. package/dist/toolchain/scaffold.d.ts +9 -0
  110. package/dist/toolchain/scaffold.js +84 -7
  111. package/dist/toolchain/west-discover.d.ts +8 -1
  112. package/dist/toolchain/west-discover.js +19 -6
  113. package/dist/toolchain/west-spawn.js +17 -5
  114. package/installer/CHANGELOG.md +473 -0
  115. package/installer/README.md +328 -0
  116. package/installer/environment.yml +46 -0
  117. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  118. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  119. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  120. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  121. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  122. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  123. package/installer/install.mjs +580 -0
  124. package/installer/install.ps1 +619 -0
  125. package/installer/install.sh +310 -0
  126. package/installer/lib/detect-platform.sh +62 -0
  127. package/installer/lib/fetch-sdk.sh +304 -0
  128. package/installer/lib/init-workspace.sh +153 -0
  129. package/installer/lib/write-activation.sh +76 -0
  130. package/installer/patches/tfm-c-include-separators.patch +19 -0
  131. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  132. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  133. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  134. package/installer/templates/project/.vscode/settings.json +30 -0
  135. package/installer/templates/project/README.md +58 -0
  136. package/installer/versions.env +110 -0
  137. package/package.json +16 -16
  138. package/src/as-built.ts +206 -0
  139. package/src/boardgen.ts +1428 -0
  140. package/src/chips/controllers.ts +61 -12
  141. package/src/chips/index.ts +35 -54
  142. package/src/chips/resolve.ts +316 -14
  143. package/src/chips/types.ts +388 -13
  144. package/src/display/profiles.ts +1 -1
  145. package/src/doctor.ts +110 -77
  146. package/src/dt-config/custom-board.ts +325 -0
  147. package/src/dt-config/kconfig.ts +177 -19
  148. package/src/dt-config/overlay.ts +1058 -450
  149. package/src/framework.manifest.ts +125 -178
  150. package/src/index.ts +57 -41
  151. package/src/lowering/adc.ts +222 -102
  152. package/src/lowering/ble.ts +32 -28
  153. package/src/lowering/dac.ts +82 -82
  154. package/src/lowering/fs.ts +18 -10
  155. package/src/lowering/gpio.ts +242 -152
  156. package/src/lowering/http.ts +512 -488
  157. package/src/lowering/hwtimer.ts +139 -101
  158. package/src/lowering/i2c.ts +116 -126
  159. package/src/lowering/index.ts +91 -93
  160. package/src/lowering/interrupts.ts +113 -21
  161. package/src/lowering/mqtt.ts +12 -6
  162. package/src/lowering/preferences.ts +327 -354
  163. package/src/lowering/pwm.ts +167 -74
  164. package/src/lowering/random.ts +3 -3
  165. package/src/lowering/sensor.ts +155 -0
  166. package/src/lowering/spi.ts +164 -124
  167. package/src/lowering/thread.ts +63 -0
  168. package/src/lowering/timing.ts +46 -72
  169. package/src/lowering/uart.ts +73 -51
  170. package/src/lowering/usb.ts +221 -0
  171. package/src/lowering/wdt.ts +95 -84
  172. package/src/lowering/wifi.ts +120 -111
  173. package/src/sdk/board-catalog-sync.ts +62 -0
  174. package/src/strategy.ts +888 -229
  175. package/src/toolchain/bossac-touch.ts +112 -0
  176. package/src/toolchain/compat.ts +8 -3
  177. package/src/toolchain/debug-config.ts +666 -522
  178. package/src/toolchain/env-check.ts +9 -9
  179. package/src/toolchain/index.ts +765 -44
  180. package/src/toolchain/scaffold.ts +81 -7
  181. package/src/toolchain/west-discover.ts +334 -316
  182. package/src/toolchain/west-spawn.ts +17 -5
  183. package/src/async/timer-polyfill.ts +0 -107
  184. package/src/chips/esp32.ts +0 -74
  185. package/src/chips/esp32s3.ts +0 -57
  186. package/src/chips/xiao-ble.ts +0 -70
  187. package/src/lowering/power.ts +0 -70
  188. package/src/lowering/pulse.ts +0 -93
  189. package/src/lowering/tone.ts +0 -60
  190. package/src/lowering/worker-backing.ts +0 -94
  191. package/src/lowering/worker.ts +0 -17
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/env node
2
+ // ---------------------------------------------------------------------------
3
+ // typeCAD Zephyr installer — cross-platform entry point.
4
+ //
5
+ // Run it:
6
+ // npx --package @typecad/framework-zephyr zephyr-installer # interactive (prints a plan, waits for Enter)
7
+ // npx --package @typecad/framework-zephyr zephyr-installer --yes # non-interactive (skip the prompt; for CI)
8
+ // node install.mjs --dry-run # print the resolved plan and exit
9
+ //
10
+ // It detects the host, prints a summary of what it will do, waits for Enter
11
+ // (unless --yes / --dry-run / non-TTY), then delegates to the native installer:
12
+ //
13
+ // POSIX → bash install.sh (args forwarded verbatim)
14
+ // Windows → pwsh install.ps1 (args translated to PowerShell param names;
15
+ // falls back to Windows PowerShell 5.1 if pwsh is absent)
16
+ //
17
+ // The OS-native scripts remain usable directly (no confirmation gate) for
18
+ // power users. translateToPwsh + buildSummary are exported for unit tests; the
19
+ // dispatch + prompt only run when this file is the node entry point.
20
+ // ---------------------------------------------------------------------------
21
+ import { spawn } from 'node:child_process';
22
+ import { basename, dirname, join, resolve } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
25
+ import { homedir } from 'node:os';
26
+ import * as readline from 'node:readline/promises';
27
+ import { stdin as input, stdout as output } from 'node:process';
28
+
29
+ const here = dirname(fileURLToPath(import.meta.url));
30
+ const isWin = process.platform === 'win32';
31
+
32
+ // Translate the user-facing POSIX-style flags into the PowerShell param names
33
+ // install.ps1 declares. Keeps the documented flag vocabulary identical across
34
+ // platforms; the OS-native scripts remain idiomatically written.
35
+ export function translateToPwsh(args) {
36
+ const out = [];
37
+ for (let i = 0; i < args.length; i++) {
38
+ switch (args[i]) {
39
+ case '--dry-run': out.push('-DryRun'); break;
40
+ case '--no-sdk': out.push('-NoSdk'); break;
41
+ case '--no-workspace': out.push('-NoWorkspace'); break;
42
+ case '--modify': out.push('-Modify'); break;
43
+ case '--prune': out.push('-Prune'); break;
44
+ case '--env-name': out.push('-EnvName', args[++i]); break;
45
+ case '--sdk-version': out.push('-SdkVersion', args[++i]); break;
46
+ case '--platforms': out.push('-Platforms', args[++i]); break;
47
+ default: out.push(args[i]);
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ // --- confirmation-gate helpers ---------------------------------------------
54
+
55
+ // Parse versions.env (KEY=value, # comments) into an object. Strips optional
56
+ // double quotes around values (needed for multi-word values like PLATFORM_esp32).
57
+ export function loadVersionsEnv(dir = here) {
58
+ const v = {};
59
+ const text = readFileSync(join(dir, 'versions.env'), 'utf8');
60
+ for (const raw of text.split(/\r?\n/)) {
61
+ const line = raw.trim();
62
+ if (!line || line.startsWith('#')) continue;
63
+ const idx = line.indexOf('=');
64
+ if (idx > 0) {
65
+ let val = line.slice(idx + 1).trim();
66
+ if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) val = val.slice(1, -1);
67
+ v[line.slice(0, idx).trim()] = val;
68
+ }
69
+ }
70
+ return v;
71
+ }
72
+
73
+ // Map node's platform/arch to the conda subdir + Zephyr SDK bundle tokens that
74
+ // the native installer will use. Mirrors lib/detect-platform.sh.
75
+ export function detectPlatform() {
76
+ const plat = process.platform;
77
+ const arm = process.arch === 'arm64';
78
+ if (plat === 'win32') return { mamba: 'win-64', sdk: 'windows-x86_64', ext: '7z' };
79
+ if (plat === 'darwin') return arm
80
+ ? { mamba: 'osx-arm64', sdk: 'macos-aarch64', ext: 'tar.xz' }
81
+ : { mamba: 'osx-64', sdk: 'macos-x86_64', ext: 'tar.xz' };
82
+ // linux
83
+ return arm
84
+ ? { mamba: 'linux-aarch64', sdk: 'linux-aarch64', ext: 'tar.xz' }
85
+ : { mamba: 'linux-64', sdk: 'linux-x86_64', ext: 'tar.xz' };
86
+ }
87
+
88
+ // Pure: build the pre-install summary string. Exported so it's unit-testable
89
+ // without running the installer. `platforms` is the selection string ('all' or
90
+ // comma-separated group ids) used to size the SDK download line.
91
+ export function buildSummary(v, p, envName, platforms = 'all') {
92
+ const home = homedir();
93
+ const mambaRoot = process.env.MAMBA_ROOT_PREFIX || join(home, 'micromamba');
94
+ const workspace = process.env.WORKSPACE_DIR || join(home, 'zephyrproject');
95
+ const sdkVer = v.ZEPHYR_SDK_VERSION || '<pinned>';
96
+ const rev = v.ZEPHYR_MANIFEST_REV || '<pinned>';
97
+ // 1.0.x bundles carry a flavor suffix (_gnu); 0.17.x "full" bundles don't.
98
+ const suffix = v.ZEPHYR_SDK_BUNDLE_SUFFIX || '';
99
+ const bundle = `zephyr-sdk-${sdkVer}_${p.sdk}${suffix}.${p.ext}`;
100
+ const sdkLine = platforms === 'all'
101
+ ? ` 3. Fetch + extract the Zephyr SDK ${sdkVer} — ${bundle} (~1.5 GB download, ~11 GB extracted).`
102
+ : ` 3. Fetch + extract the Zephyr SDK ${sdkVer} (minimal + selected toolchains — platforms: ${platforms}).`;
103
+ return [
104
+ '',
105
+ 'typeCAD Zephyr installer',
106
+ '========================',
107
+ '',
108
+ 'This sets up a complete Zephyr RTOS build environment via micromamba — no',
109
+ 'preinstalled conda, Python, or toolchain required. It will:',
110
+ '',
111
+ ` 1. Download the micromamba static binary (${p.mamba}).`,
112
+ ` 2. Create the '${envName}' conda env (west, cmake, ninja, gperf, ...).`,
113
+ sdkLine,
114
+ ` 4. Run 'west init' (--mr ${rev}) + 'west update' for a vanilla Zephyr workspace.`,
115
+ '',
116
+ 'Locations (defaults; override via MAMBA_ROOT_PREFIX / WORKSPACE_DIR / SDK_INSTALL_PARENT):',
117
+ ` micromamba root : ${mambaRoot}`,
118
+ ` conda env : ${join(mambaRoot, 'envs', envName)}`,
119
+ ` Zephyr SDK : ${join(mambaRoot, 'zephyr-sdk', `zephyr-sdk-${sdkVer}`)}`,
120
+ ` west workspace : ${workspace} (ZEPHYR_BASE = ${join(workspace, 'zephyr')})`,
121
+ '',
122
+ 'No sudo, no Zephyr SDK setup.sh — the SDK is used in place via ZEPHYR_SDK_INSTALL_DIR.',
123
+ '',
124
+ ].join('\n');
125
+ }
126
+
127
+ // Read --env-name from argv (default 'zephyr') so the summary matches what the
128
+ // native installer will actually name the env.
129
+ function envNameFromArgs(args) {
130
+ const i = args.indexOf('--env-name');
131
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : 'zephyr';
132
+ }
133
+
134
+ // --- platform checklist ------------------------------------------------------
135
+
136
+ // The platform groups from versions.env (PLATFORM_<id>, PLATFORM_<id>_LABEL,
137
+ // PLATFORM_<id>_SIZE). Returns [{id, label, size, toolchains}].
138
+ export function platformCatalog(v = loadVersionsEnv()) {
139
+ const groups = [];
140
+ for (const key of Object.keys(v)) {
141
+ const m = key.match(/^PLATFORM_([a-z0-9]+)$/);
142
+ if (!m) continue;
143
+ const id = m[1];
144
+ groups.push({
145
+ id,
146
+ label: v[`PLATFORM_${id}_LABEL`] || id,
147
+ size: v[`PLATFORM_${id}_SIZE`] || '',
148
+ toolchains: (v[key] || '').split(/\s+/).filter(Boolean),
149
+ });
150
+ }
151
+ return groups;
152
+ }
153
+
154
+ // Parse the user's checklist answer ('1 2', '1,2', 'all', 'arm esp32') into a
155
+ // normalized comma-separated group-id string ('arm,esp32') or 'all'.
156
+ // Numbers index into catalog (1-based); names must match group ids.
157
+ export function parsePlatformSelection(answer, catalog = platformCatalog()) {
158
+ const trimmed = (answer || '').trim().toLowerCase();
159
+ if (!trimmed || trimmed === 'all' || trimmed === 'a') return 'all';
160
+ const picked = new Set();
161
+ for (const tok of trimmed.split(/[\s,]+/).filter(Boolean)) {
162
+ if (/^\d+$/.test(tok)) {
163
+ const idx = Number(tok) - 1;
164
+ if (idx >= 0 && idx < catalog.length) picked.add(catalog[idx].id);
165
+ else throw new Error(`invalid platform number: ${tok} (choose 1-${catalog.length} or 'all')`);
166
+ } else {
167
+ const grp = catalog.find((g) => g.id === tok);
168
+ if (!grp) throw new Error(`unknown platform: ${tok} (choose a number, a group id, or 'all')`);
169
+ picked.add(grp.id);
170
+ }
171
+ }
172
+ if (picked.size === 0) return 'all';
173
+ return [...picked].join(',');
174
+ }
175
+
176
+ // Render the checklist text. `installedToolchains` (array of toolchain target
177
+ // dir names found under the SDK root) marks groups already installed.
178
+ // 'All' is listed FIRST and is the default (Enter accepts it) — the recommended
179
+ // path is to install everything so any board in the pack just builds; the
180
+ // per-group entries are the space-saving opt-in.
181
+ export function buildChecklist(catalog = platformCatalog(), installedToolchains = []) {
182
+ const inst = new Set(installedToolchains);
183
+ const lines = [
184
+ '',
185
+ 'Select platform toolchains to install:',
186
+ '',
187
+ " [a] All platforms — RECOMMENDED (~1.5 GB download / ~11 GB extracted).",
188
+ ' Press Enter to accept. Installs every toolchain so any Zephyr board',
189
+ " in the data pack builds as-is. Pick individual groups only if you're",
190
+ ' consciously trading convenience for disk space.',
191
+ '',
192
+ ];
193
+ catalog.forEach((g, i) => {
194
+ const allIn = g.toolchains.length > 0 && g.toolchains.every((t) => inst.has(t));
195
+ const someIn = g.toolchains.some((t) => inst.has(t));
196
+ const mark = allIn ? '[x]' : someIn ? '[~]' : '[ ]';
197
+ const instTag = allIn ? ' installed' : someIn ? ' partial' : '';
198
+ const size = g.size ? ` ${g.size}` : '';
199
+ lines.push(` [${i + 1}] ${g.label.padEnd(48)}${size}${instTag}`);
200
+ });
201
+ lines.push('');
202
+ lines.push("Enter selection (Enter = All; or e.g. '1 2', 'arm,esp32' for a minimal install): ");
203
+ return lines.join('\n');
204
+ }
205
+
206
+ // Interactive checklist: print, read one line, parse. Returns the normalized
207
+ // selection string ('all' or comma-separated group ids).
208
+ export async function platformChecklist(installedToolchains = []) {
209
+ const catalog = platformCatalog();
210
+ output.write(buildChecklist(catalog, installedToolchains));
211
+ const rl = readline.createInterface({ input, output });
212
+ try {
213
+ const answer = await rl.question('');
214
+ return parsePlatformSelection(answer, catalog);
215
+ } finally {
216
+ rl.close();
217
+ }
218
+ }
219
+
220
+ // Try each candidate executable in order; advance on ENOENT so the caller can
221
+ // express "prefer pwsh, fall back to powershell" without probing PATH itself.
222
+ function runWithFallback(exes, args) {
223
+ return new Promise((resolve, reject) => {
224
+ const tryOne = (idx) => {
225
+ if (idx >= exes.length) {
226
+ reject(new Error(`none found on PATH: ${exes.join(', ')}`));
227
+ return;
228
+ }
229
+ const child = spawn(exes[idx], args, { stdio: 'inherit' });
230
+ child.on('error', (err) => {
231
+ if (err && err.code === 'ENOENT') tryOne(idx + 1);
232
+ else reject(err);
233
+ });
234
+ child.on('exit', (code, signal) => resolve({ code, signal }));
235
+ };
236
+ tryOne(0);
237
+ });
238
+ }
239
+
240
+ // Render the --help text. Shows flags, platform groups, and examples.
241
+ // Exported so tests can assert on the content.
242
+ export function buildHelp(v = loadVersionsEnv()) {
243
+ const catalog = platformCatalog(v);
244
+ const groups = catalog
245
+ .map((g, i) => ` ${String(i + 1).padStart(2)}. ${g.label.padEnd(48)} ${g.size}`)
246
+ .join('\n');
247
+ return `
248
+ typeCAD Zephyr installer — cross-platform Zephyr toolchain setup via micromamba.
249
+
250
+ Usage:
251
+ npx --package @typecad/framework-zephyr zephyr-installer [flags]
252
+
253
+ Installs: micromamba + conda env (west, cmake, ninja, dtc, gperf) + the Zephyr
254
+ SDK (selective toolchains or full bundle) + a vanilla west workspace
255
+ (${v.ZEPHYR_MANIFEST_URL || 'zephyr.git'} @ ${v.ZEPHYR_MANIFEST_REV || 'pinned'}).
256
+
257
+ Platform groups (interactive checklist, or --platforms <ids>):
258
+ ${groups}
259
+ all. Full bundle (~1.5 GB download, ~11 GB extracted)
260
+
261
+ Flags:
262
+ (none) Interactive: platform checklist → summary → Enter → install.
263
+ --platforms IDS Space-saving subset (comma-separated group ids). The
264
+ default is 'all' — every toolchain, so any board in the
265
+ data pack builds without a follow-up install.
266
+ --modify Re-run the SDK platform step on an existing install:
267
+ ADDS the selected groups' missing toolchains. Nothing is
268
+ deleted unless --prune is also given. SDK-only (skips
269
+ env/workspace).
270
+ --prune With --modify: also DELETE toolchains of platforms not in
271
+ the selection (reclaim disk space). Without it, --modify
272
+ is purely additive.
273
+ --delete UNINSTALL everything: conda env, Zephyr SDK, west
274
+ workspace (+ micromamba itself if no other envs exist).
275
+ Requires typing 'yes' to confirm (or --yes).
276
+ --yes, -y Skip the confirmation prompt (CI / scripting).
277
+ --dry-run Print the resolved plan (URLs, paths, versions) and exit.
278
+ --no-sdk Skip the Zephyr SDK download (env + workspace only).
279
+ --no-workspace Skip west init/update (env + SDK only).
280
+ --env-name NAME Override the conda env name (default: ${v.ENV_NAME || 'zephyr'}).
281
+ --sdk-version V Override the Zephyr SDK version (default: ${v.ZEPHYR_SDK_VERSION || 'pinned'}).
282
+ -h, --help Show this help.
283
+
284
+ Environment overrides:
285
+ MAMBA_ROOT_PREFIX micromamba root (default: ~/micromamba)
286
+ WORKSPACE_DIR west workspace (default: ~/zephyrproject)
287
+ SDK_INSTALL_PARENT where the SDK extracts (default: $MAMBA_ROOT_PREFIX/zephyr-sdk)
288
+
289
+ Examples:
290
+ npx --package @typecad/framework-zephyr zephyr-installer # RECOMMENDED: everything (~1.5 GB)
291
+ npx --package @typecad/framework-zephyr zephyr-installer --platforms arm # space-saver: ARM Cortex-M only (~150 MB)
292
+ npx --package @typecad/framework-zephyr zephyr-installer --platforms arm,esp32 # space-saver: two groups (~450 MB)
293
+ npx --package @typecad/framework-zephyr zephyr-installer --modify --yes # later: add any missing toolchains
294
+ npx --package @typecad/framework-zephyr zephyr-installer --modify --prune --yes # ...and also remove unselected ones
295
+ npx --package @typecad/framework-zephyr zephyr-installer --delete # uninstall everything (confirms)
296
+ npx --package @typecad/framework-zephyr zephyr-installer --dry-run # preview the plan
297
+ node install.mjs --help # same, from a repo checkout
298
+ `.trimStart();
299
+ }
300
+
301
+ // --- --delete: uninstall everything the installer created -------------------
302
+
303
+ // Recursively sum bytes on disk for a path (best-effort; 0 on any error).
304
+ function dirSize(p) {
305
+ let total = 0;
306
+ const walk = (d) => {
307
+ let entries;
308
+ try { entries = readdirSync(d, { withFileTypes: true }); } catch { return; }
309
+ for (const e of entries) {
310
+ const full = join(d, e.name);
311
+ try {
312
+ if (e.isDirectory()) walk(full);
313
+ else total += statSync(full).size;
314
+ } catch { /* unreadable entry — skip */ }
315
+ }
316
+ };
317
+ try { walk(p); } catch { /* unreadable root */ }
318
+ return total;
319
+ }
320
+
321
+ function humanSize(bytes) {
322
+ if (bytes === 0) return '0 B';
323
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
324
+ const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
325
+ return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
326
+ }
327
+
328
+ // Resolve the installation layout the same way install.sh/install.ps1 do.
329
+ export function resolveInstallPaths(envName = 'zephyr') {
330
+ const mambaRoot = process.env.MAMBA_ROOT_PREFIX || join(homedir(), 'micromamba');
331
+ return {
332
+ mambaRoot,
333
+ env: join(mambaRoot, 'envs', envName),
334
+ sdkParent: process.env.SDK_INSTALL_PARENT || join(mambaRoot, 'zephyr-sdk'),
335
+ workspace: process.env.WORKSPACE_DIR || join(homedir(), 'zephyrproject'),
336
+ };
337
+ }
338
+
339
+ // Build the --delete confirmation summary: every path that WILL be removed,
340
+ // with its on-disk size. Pure — exported for unit tests.
341
+ export function buildDeleteSummary(paths = resolveInstallPaths()) {
342
+ const items = [
343
+ ['conda env', paths.env],
344
+ ['Zephyr SDK', paths.sdkParent],
345
+ ['west workspace', paths.workspace],
346
+ ];
347
+ const lines = [
348
+ '',
349
+ 'typeCAD Zephyr installer — DELETE installation',
350
+ '==============================================',
351
+ '',
352
+ 'This permanently removes everything the installer created:',
353
+ '',
354
+ ];
355
+ for (const [label, p] of items) {
356
+ if (existsSync(p)) {
357
+ lines.push(` ${label.padEnd(16)} ${p} (${humanSize(dirSize(p))})`);
358
+ } else {
359
+ lines.push(` ${label.padEnd(16)} ${p} (not present — skipped)`);
360
+ }
361
+ }
362
+ // micromamba itself: only removable when no other envs exist.
363
+ const otherEnvs = existsSync(paths.env)
364
+ ? readdirSync(join(paths.mambaRoot, 'envs'), { withFileTypes: true })
365
+ .filter((e) => e.isDirectory() && e.name !== basename(paths.env))
366
+ .map((e) => e.name)
367
+ : [];
368
+ lines.push('');
369
+ if (otherEnvs.length === 0 && existsSync(paths.mambaRoot)) {
370
+ lines.push(` micromamba root ${paths.mambaRoot} (${humanSize(dirSize(paths.mambaRoot))})`);
371
+ lines.push(' (micromamba itself — removed because it has no other envs)');
372
+ } else if (otherEnvs.length > 0) {
373
+ lines.push(` micromamba root ${paths.mambaRoot} — KEPT (other envs present: ${otherEnvs.join(', ')})`);
374
+ }
375
+ lines.push('');
376
+ lines.push('The shell-profile hook (micromamba shell init) is NOT edited automatically;');
377
+ lines.push('remove its block from ~/.bashrc / your PowerShell $PROFILE if desired.');
378
+ lines.push('');
379
+ lines.push("Type 'yes' to DELETE these directories, anything else to cancel: ");
380
+ return lines.join('\n');
381
+ }
382
+
383
+ // Perform the deletion. Returns per-item results for reporting.
384
+ export function performDelete(paths = resolveInstallPaths()) {
385
+ const envName = basename(paths.env);
386
+ const targets = [
387
+ ['conda env', paths.env],
388
+ ['Zephyr SDK', paths.sdkParent],
389
+ ['west workspace', paths.workspace],
390
+ ];
391
+ // micromamba root only when no other envs remain after removing ours.
392
+ const otherEnvs = existsSync(join(paths.mambaRoot, 'envs'))
393
+ ? readdirSync(join(paths.mambaRoot, 'envs'), { withFileTypes: true })
394
+ .filter((e) => e.isDirectory() && e.name !== envName)
395
+ : [];
396
+ if (otherEnvs.length === 0 && existsSync(paths.mambaRoot)) {
397
+ targets.push(['micromamba root', paths.mambaRoot]);
398
+ }
399
+ const results = [];
400
+ for (const [label, p] of targets) {
401
+ if (!existsSync(p)) {
402
+ results.push({ label, path: p, removed: false, note: 'not present' });
403
+ continue;
404
+ }
405
+ try {
406
+ rmSync(p, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
407
+ results.push({ label, path: p, removed: !existsSync(p) });
408
+ } catch (err) {
409
+ results.push({ label, path: p, removed: false, note: err.message });
410
+ }
411
+ }
412
+ return results;
413
+ }
414
+
415
+ // Only dispatch when invoked directly as `node install.mjs` / via the bin, not
416
+ // when imported (the test suite imports translateToPwsh / buildSummary). Resolve
417
+ // symlinks on both sides: npx and global installs run the bin through a symlink,
418
+ // and path.resolve alone doesn't follow it — without realpathSync the guard
419
+ // evaluates false and the script exits without dispatching (npx "does nothing").
420
+ const realPath = (p) => { try { return realpathSync(p); } catch { return ''; } };
421
+ const invokedDirectly =
422
+ !!process.argv[1] && realPath(fileURLToPath(import.meta.url)) === realPath(process.argv[1]);
423
+
424
+ if (invokedDirectly) {
425
+ const rawArgs = process.argv.slice(2);
426
+ // --help / -h: print + exit before any prompting or dispatch.
427
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
428
+ output.write(buildHelp());
429
+ process.exit(0);
430
+ }
431
+ const dryRun = rawArgs.includes('--dry-run');
432
+ const yes = rawArgs.includes('--yes') || rawArgs.includes('-y');
433
+ const modify = rawArgs.includes('--modify') || rawArgs.includes('-m');
434
+ const prune = rawArgs.includes('--prune');
435
+ const del = rawArgs.includes('--delete') || rawArgs.includes('-d');
436
+
437
+ // --platforms <sel> (non-interactive selection) — read + strip here so the
438
+ // checklist doesn't prompt when it's given.
439
+ let platforms = null;
440
+ {
441
+ const i = rawArgs.indexOf('--platforms');
442
+ if (i >= 0 && i + 1 < rawArgs.length) platforms = rawArgs[i + 1];
443
+ }
444
+ // --yes / -y / --modify / -m / --platforms <v> are consumed here; everything
445
+ // else (--dry-run, --no-sdk, --env-name, ...) is forwarded.
446
+ const forwarded = rawArgs.filter(
447
+ (a, i) =>
448
+ a !== '--yes' && a !== '-y' && a !== '--modify' && a !== '-m' &&
449
+ a !== '--prune' &&
450
+ a !== '--platforms' && rawArgs[i - 1] !== '--platforms',
451
+ );
452
+ // Re-add the resolved platforms as a flag the native scripts understand.
453
+ if (platforms) forwarded.push('--platforms', platforms);
454
+ if (prune) forwarded.push('--prune');
455
+ // --modify skips the env + workspace steps; only the SDK platform step runs.
456
+ if (modify) forwarded.push('--no-workspace');
457
+
458
+ (async () => {
459
+ // --delete: confirm + remove everything, entirely in Node (no dispatch to
460
+ // the native installers — nothing to install). Requires an explicit typed
461
+ // 'yes' (or --yes for scripting); non-TTY without --yes aborts (destructive
462
+ // default-deny, unlike install's proceed-on-non-TTY).
463
+ if (del) {
464
+ const summary = buildDeleteSummary();
465
+ output.write(summary);
466
+ let confirmed = yes;
467
+ if (!yes) {
468
+ if (input.isTTY) {
469
+ const rl = readline.createInterface({ input, output });
470
+ try { confirmed = (await rl.question('')).trim().toLowerCase() === 'yes'; }
471
+ finally { rl.close(); }
472
+ } else {
473
+ output.write('--delete is destructive and stdin is not interactive. Re-run with --yes to proceed.\n');
474
+ process.exit(1);
475
+ }
476
+ }
477
+ if (!confirmed) {
478
+ output.write('Cancelled — nothing was deleted.\n');
479
+ process.exit(0);
480
+ }
481
+ const results = performDelete();
482
+ for (const r of results) {
483
+ if (r.removed) output.write(` deleted ${r.label.padEnd(16)} ${r.path}\n`);
484
+ else output.write(` skipped ${r.label.padEnd(16)} ${r.path}${r.note ? ` (${r.note})` : ' (not present)'}\n`);
485
+ }
486
+ output.write('\nUninstall complete. (Shell-profile hook left in place — see note above.)\n');
487
+ process.exit(0);
488
+ }
489
+
490
+ // --modify: warn before any destructive change (deselected toolchains are
491
+ // deleted from disk). The user must acknowledge before the checklist runs.
492
+ if (modify && !dryRun && !yes) {
493
+ output.write(
494
+ [
495
+ '',
496
+ "--modify ADDS the missing toolchains of the selected platforms. Toolchains",
497
+ 'of unselected platforms are only removed when --prune is also given;',
498
+ 're-downloading a removed platform costs ~100-300 MB per group.',
499
+ '',
500
+ ].join('\n'),
501
+ );
502
+ if (input.isTTY) {
503
+ const rl = readline.createInterface({ input, output });
504
+ try {
505
+ await rl.question('Press Enter to continue (Ctrl+C to cancel): ');
506
+ } finally {
507
+ rl.close();
508
+ }
509
+ }
510
+ }
511
+
512
+ // Resolve the platform selection (interactive checklist unless given).
513
+ if (platforms === null && !dryRun) {
514
+ if (input.isTTY) {
515
+ try {
516
+ platforms = await platformChecklist([]);
517
+ } catch (err) {
518
+ console.error(`typecad-zephyr-install: ${err.message}`);
519
+ process.exit(1);
520
+ }
521
+ // Replace any previously forwarded --platforms with the resolved value.
522
+ const pi = forwarded.lastIndexOf('--platforms');
523
+ if (pi >= 0) forwarded.splice(pi, 2);
524
+ forwarded.push('--platforms', platforms);
525
+ } else {
526
+ // Non-interactive with no --platforms: default to 'all' (full bundle,
527
+ // current behavior) so npx/CI invocations don't hang.
528
+ platforms = 'all';
529
+ forwarded.push('--platforms', 'all');
530
+ }
531
+ } else if (platforms === null) {
532
+ // --dry-run without --platforms: forward without a selection; the native
533
+ // dry-run prints both modes.
534
+ platforms = 'all';
535
+ }
536
+
537
+ if (!dryRun) {
538
+ output.write(buildSummary(loadVersionsEnv(), detectPlatform(), envNameFromArgs(rawArgs), platforms));
539
+ if (!yes) {
540
+ if (input.isTTY) {
541
+ const rl = readline.createInterface({ input, output });
542
+ try {
543
+ await rl.question('Press Enter to begin (Ctrl+C to cancel): ');
544
+ } finally {
545
+ rl.close();
546
+ }
547
+ } else {
548
+ // Non-interactive stdin (some npx invocations, pipes, CI): can't
549
+ // prompt, so proceed — the user invoked us explicitly. (--yes is the
550
+ // explicit no-prompt flag.) Avoids `npx @typecad/...` aborting when
551
+ // npx doesn't forward a TTY.
552
+ }
553
+ }
554
+ }
555
+
556
+ const promise = isWin
557
+ ? runWithFallback(
558
+ ['pwsh', 'powershell'],
559
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', join(here, 'install.ps1'), ...translateToPwsh(forwarded)],
560
+ )
561
+ : runWithFallback(['bash'], [join(here, 'install.sh'), ...forwarded]);
562
+
563
+ return promise
564
+ .then(({ code, signal }) => {
565
+ if (signal) process.exit(128);
566
+ process.exit(code ?? 1);
567
+ })
568
+ .catch((err) => {
569
+ console.error(`typecad-zephyr-install: ${err.message}`);
570
+ if (isWin) {
571
+ console.error(' Windows needs PowerShell, which ships with Windows 10+.');
572
+ console.error(' If you have Git Bash, you can instead run: bash install.sh');
573
+ } else {
574
+ console.error(' POSIX hosts need bash. Install it, or on Windows use:');
575
+ console.error(' pwsh -File install.ps1 (or powershell -File install.ps1)');
576
+ }
577
+ process.exit(1);
578
+ });
579
+ })();
580
+ }