@kvell007/embed-labs-cli 0.1.0-alpha.30 → 0.1.0-alpha.32

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.
@@ -83,6 +83,7 @@ export interface LocalToolchainPackageManifest {
83
83
  family?: string;
84
84
  board?: string;
85
85
  variant?: string;
86
+ board_id?: string;
86
87
  requires?: Array<{
87
88
  id: string;
88
89
  version: string;
@@ -148,6 +149,7 @@ export interface LocalToolchainCurrentResult {
148
149
  }
149
150
  export interface LocalToolchainListOptions extends LocalToolchainLatestOptions {
150
151
  installRoot?: string;
152
+ installedOnly?: boolean;
151
153
  }
152
154
  export interface LocalToolchainEnvironmentRecord {
153
155
  board_id: string;
@@ -193,6 +195,7 @@ export interface LocalToolchainEnvironmentComponent {
193
195
  export interface LocalToolchainListResult {
194
196
  host: string;
195
197
  channel: string;
198
+ metadata_source: "built_in" | "local_override";
196
199
  metadata_root?: string;
197
200
  install_root: string;
198
201
  registry_path: string;
@@ -258,6 +261,7 @@ export declare function installLocalToolchain(options?: LocalToolchainInstallOpt
258
261
  export declare function validateLocalToolchain(input?: string | {
259
262
  releaseRoot?: string;
260
263
  mode?: string;
264
+ boardId?: string;
261
265
  }): Promise<LocalToolchainValidationResult>;
262
266
  export declare function compileTaishanPiSingleFile(options: LocalCompileOptions): Promise<LocalCompileResult>;
263
267
  export declare function buildTaishanPiQtSmoke(options: LocalQtSmokeBuildOptions): Promise<LocalCompileResult>;
@@ -9,8 +9,9 @@ import { pipeline } from "node:stream/promises";
9
9
  import { fileURLToPath } from "node:url";
10
10
  const DEFAULT_RELEASE_ROOT = "/Volumes/LLVM-TSPI/tspi-rk3566-llvm-release-minimal";
11
11
  const DEFAULT_QT_SMOKE_SOURCE = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/qt-smoke";
12
- const DEFAULT_METADATA_ROOT = "/Users/kvell/kk-project/DBT-Agent-Project/llvm-build-tspi/embedlabs-release";
13
12
  const DEFAULT_BOARD_ID = "taishanpi-1m-rk3566";
13
+ const PICO2W_RP2350_BOARD_ID = "pico2w-rp2350-monitor";
14
+ const COLOREASYPICO2_RP2350_BOARD_ID = "coloreasypico2-rp2350-monitor";
14
15
  const DEFAULT_CHANNEL = "stable";
15
16
  const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
16
17
  const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
@@ -24,7 +25,10 @@ const BUILT_IN_CHANNEL = {
24
25
  { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.31", manifest: "" },
25
26
  { id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.31", manifest: "" },
26
27
  { id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
27
- { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" }
28
+ { id: "embedlabs.family.rp2350", version: "1.0.0", manifest: "" },
29
+ { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" },
30
+ { id: "embedlabs.board.pico2w.rp2350-monitor", version: "1.0.31", manifest: "" },
31
+ { id: "embedlabs.board.coloreasypico2.rp2350-monitor", version: "1.0.31", manifest: "" }
28
32
  ]
29
33
  };
30
34
  const BUILT_IN_MANIFESTS = {
@@ -80,6 +84,16 @@ const BUILT_IN_MANIFESTS = {
80
84
  { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
81
85
  ]
82
86
  },
87
+ "embedlabs.family.rp2350": {
88
+ schema: "embedlabs.package.v1",
89
+ id: "embedlabs.family.rp2350",
90
+ version: "1.0.0",
91
+ kind: "family",
92
+ family: "rp2350",
93
+ requires: [
94
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31" }
95
+ ]
96
+ },
83
97
  "embedlabs.board.taishanpi.1m-rk3566": {
84
98
  schema: "embedlabs.package.v1",
85
99
  id: "embedlabs.board.taishanpi.1m-rk3566",
@@ -93,10 +107,41 @@ const BUILT_IN_MANIFESTS = {
93
107
  { id: "embedlabs.tools.vendor.rockchip", version: "^1.0.0", roles: ["flash", "resource-image"] },
94
108
  { id: "embedlabs.tools.common.llvm", version: "22.x", roles: ["compile"] },
95
109
  { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0", roles: ["userdata-image"] },
96
- { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.31", roles: ["qtquick-preview"] },
97
- { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["rp2350-monitor"] }
110
+ { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "^1.0.31", roles: ["qtquick-preview"] }
98
111
  ],
99
112
  build_modes: ["local-llvm"]
113
+ },
114
+ "embedlabs.board.pico2w.rp2350-monitor": {
115
+ schema: "embedlabs.package.v1",
116
+ id: "embedlabs.board.pico2w.rp2350-monitor",
117
+ version: "1.0.31",
118
+ kind: "board",
119
+ display_name: "Pico 2 W / RP2350 Monitor",
120
+ family: "rp2350",
121
+ board: "Pico 2 W",
122
+ variant: "RP2350 Monitor",
123
+ board_id: PICO2W_RP2350_BOARD_ID,
124
+ requires: [
125
+ { id: "embedlabs.family.rp2350", version: "^1.0.0" },
126
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
127
+ ],
128
+ build_modes: ["local-monitor"]
129
+ },
130
+ "embedlabs.board.coloreasypico2.rp2350-monitor": {
131
+ schema: "embedlabs.package.v1",
132
+ id: "embedlabs.board.coloreasypico2.rp2350-monitor",
133
+ version: "1.0.31",
134
+ kind: "board",
135
+ display_name: "ColorEasyPICO2 / RP2350 Monitor",
136
+ family: "rp2350",
137
+ board: "ColorEasyPICO2",
138
+ variant: "RP2350 Monitor",
139
+ board_id: COLOREASYPICO2_RP2350_BOARD_ID,
140
+ requires: [
141
+ { id: "embedlabs.family.rp2350", version: "^1.0.0" },
142
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
143
+ ],
144
+ build_modes: ["local-monitor"]
100
145
  }
101
146
  };
102
147
  const INSTALL_COPY_PATHS = [
@@ -109,7 +154,6 @@ const INSTALL_COPY_PATHS = [
109
154
  "tools/mac",
110
155
  "toolkit-runtime/qtquick-live-preview",
111
156
  "toolkit-runtime/rp2350-monitor",
112
- "toolkit-runtime/RP2350-Monitor",
113
157
  "images/current",
114
158
  "userdata/rootfs",
115
159
  "boot-workspace",
@@ -119,7 +163,7 @@ const INSTALL_COPY_PATHS = [
119
163
  "support",
120
164
  "third_party"
121
165
  ];
122
- const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "compile", "qt", "full", "images"];
166
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
123
167
  export function defaultLocalReleaseRoot() {
124
168
  return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
125
169
  || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
@@ -134,12 +178,13 @@ export async function latestLocalToolchain(options = {}) {
134
178
  if (!board) {
135
179
  throw new Error(`No local toolchain board package found for ${boardId}.`);
136
180
  }
181
+ const canonicalBoardId = boardIdForPackageManifest(board);
137
182
  const packages = resolvePackageRefs(boardPackageId, channel, manifests);
138
183
  let download;
139
184
  let downloadError;
140
185
  try {
141
186
  download = await resolveLocalToolchainDownloadPlan({
142
- boardId,
187
+ boardId: canonicalBoardId,
143
188
  channel: channelName,
144
189
  host: hostId(),
145
190
  toolchain: "llvm"
@@ -149,7 +194,7 @@ export async function latestLocalToolchain(options = {}) {
149
194
  downloadError = error instanceof Error ? error.message : String(error);
150
195
  }
151
196
  return {
152
- board_id: boardId,
197
+ board_id: canonicalBoardId,
153
198
  channel: channel.channel,
154
199
  host: hostId(),
155
200
  version: download?.version ?? board.version,
@@ -164,10 +209,34 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
164
209
  const registryPath = localToolchainRegistryPath(root);
165
210
  try {
166
211
  const registry = JSON.parse(await readFile(registryPath, "utf8"));
212
+ const environments = registry.environments;
213
+ const boardInstall = environments?.[normalizeBoardId(boardId)];
214
+ if (boardInstall?.release_root) {
215
+ return {
216
+ installed: true,
217
+ board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
218
+ version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
219
+ mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
220
+ release_root: boardInstall.release_root,
221
+ registry_path: registryPath,
222
+ install_root: root,
223
+ channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
224
+ packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
225
+ };
226
+ }
167
227
  const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
228
+ const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
229
+ if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
230
+ return {
231
+ installed: false,
232
+ board_id: boardId,
233
+ registry_path: registryPath,
234
+ install_root: root
235
+ };
236
+ }
168
237
  return {
169
238
  installed: !!releaseRoot,
170
- board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
239
+ board_id: registryBoardId,
171
240
  version: typeof registry.version === "string" ? registry.version : undefined,
172
241
  mode: typeof registry.mode === "string" ? registry.mode : undefined,
173
242
  release_root: releaseRoot,
@@ -186,6 +255,58 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
186
255
  };
187
256
  }
188
257
  }
258
+ async function discoverInstalledLocalToolchains(installRoot, current) {
259
+ const installed = new Map();
260
+ if (current.installed && current.release_root) {
261
+ installed.set(normalizeBoardId(current.board_id), {
262
+ board_id: current.board_id,
263
+ version: current.version,
264
+ channel: current.channel,
265
+ mode: current.mode,
266
+ release_root: current.release_root
267
+ });
268
+ }
269
+ const toolchainsRoot = join(installRoot, "toolchains");
270
+ let boardEntries;
271
+ try {
272
+ boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
273
+ }
274
+ catch {
275
+ return installed;
276
+ }
277
+ for (const boardEntry of boardEntries) {
278
+ if (!boardEntry.isDirectory()) {
279
+ continue;
280
+ }
281
+ const boardId = normalizeBoardId(boardEntry.name);
282
+ if (installed.has(boardId)) {
283
+ continue;
284
+ }
285
+ const boardRoot = join(toolchainsRoot, boardEntry.name);
286
+ let versionEntries;
287
+ try {
288
+ versionEntries = await readdir(boardRoot, { withFileTypes: true });
289
+ }
290
+ catch {
291
+ continue;
292
+ }
293
+ const versions = versionEntries
294
+ .filter((entry) => entry.isDirectory())
295
+ .map((entry) => entry.name)
296
+ .sort(compareVersionLike)
297
+ .reverse();
298
+ const version = versions[0];
299
+ if (!version) {
300
+ continue;
301
+ }
302
+ installed.set(boardId, {
303
+ board_id: boardId,
304
+ version,
305
+ release_root: join(boardRoot, version)
306
+ });
307
+ }
308
+ return installed;
309
+ }
189
310
  export async function listLocalToolchainEnvironments(options = {}) {
190
311
  const channelName = options.channel ?? DEFAULT_CHANNEL;
191
312
  const host = hostId();
@@ -193,6 +314,7 @@ export async function listLocalToolchainEnvironments(options = {}) {
193
314
  const registryPath = localToolchainRegistryPath(installRoot);
194
315
  const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
195
316
  const current = await currentLocalToolchain(installRoot);
317
+ const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
196
318
  const boardManifests = [...manifests.values()]
197
319
  .filter((manifest) => manifest.kind === "board")
198
320
  .filter((manifest) => {
@@ -224,12 +346,16 @@ export async function listLocalToolchainEnvironments(options = {}) {
224
346
  downloadError = error instanceof Error ? error.message : String(error);
225
347
  }
226
348
  const latestVersion = download?.version ?? board.version;
227
- const installed = current.installed && normalizeBoardId(current.board_id) === normalizeBoardId(boardId)
349
+ const currentForBoard = await currentLocalToolchain(installRoot, boardId);
350
+ const installedCandidate = currentForBoard.installed
351
+ ? currentForBoard
352
+ : installedByBoard.get(normalizeBoardId(boardId));
353
+ const installed = installedCandidate
228
354
  ? {
229
- version: current.version,
230
- channel: current.channel,
231
- mode: current.mode,
232
- release_root: current.release_root
355
+ version: installedCandidate.version,
356
+ channel: installedCandidate.channel,
357
+ mode: installedCandidate.mode,
358
+ release_root: installedCandidate.release_root
233
359
  }
234
360
  : undefined;
235
361
  const updateAvailable = !!installed?.version && installed.version !== latestVersion;
@@ -241,6 +367,7 @@ export async function listLocalToolchainEnvironments(options = {}) {
241
367
  ? "installed"
242
368
  : "available";
243
369
  const mode = download?.default_mode ?? "qt";
370
+ const installModes = localToolchainInstallModesForDownload(download);
244
371
  environments.push({
245
372
  board_id: boardId,
246
373
  package_id: board.id,
@@ -252,7 +379,7 @@ export async function listLocalToolchainEnvironments(options = {}) {
252
379
  status,
253
380
  supported_host: hostSupport.supported,
254
381
  unsupported_packages: hostSupport.unsupportedPackages,
255
- install_modes: [...LOCAL_TOOLCHAIN_INSTALL_MODES],
382
+ install_modes: installModes,
256
383
  installed,
257
384
  latest: {
258
385
  version: latestVersion,
@@ -266,16 +393,20 @@ export async function listLocalToolchainEnvironments(options = {}) {
266
393
  components: download?.components?.map(localToolchainEnvironmentComponent),
267
394
  install_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode}`,
268
395
  update_command: `embedlabs local toolchain install --board ${boardId} --mode ${mode} --force`,
269
- notes: environmentNotes({ status, downloadError, unsupportedPackages: hostSupport.unsupportedPackages })
396
+ notes: environmentNotes({ boardId, status, downloadError, unsupportedPackages: hostSupport.unsupportedPackages })
270
397
  });
271
398
  }
399
+ const filteredEnvironments = options.installedOnly
400
+ ? environments.filter((environment) => !!environment.installed)
401
+ : environments;
272
402
  return {
273
403
  host,
274
404
  channel: channel.channel,
405
+ metadata_source: metadataRoot ? "local_override" : "built_in",
275
406
  metadata_root: metadataRoot,
276
407
  install_root: installRoot,
277
408
  registry_path: registryPath,
278
- environments
409
+ environments: filteredEnvironments
279
410
  };
280
411
  }
281
412
  export async function installLocalToolchain(options = {}) {
@@ -284,7 +415,7 @@ export async function installLocalToolchain(options = {}) {
284
415
  const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
285
416
  const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
286
417
  if (await pathExists(releaseRoot) && !options.force) {
287
- const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
418
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
288
419
  if (!validation.ok) {
289
420
  if (latest.download?.components?.length) {
290
421
  // Component installs can upgrade an existing lower-mode install by overlaying
@@ -320,7 +451,7 @@ export async function installLocalToolchain(options = {}) {
320
451
  }
321
452
  await mkdir(releaseRoot, { recursive: true });
322
453
  const installedPaths = [];
323
- for (const relativePath of INSTALL_COPY_PATHS) {
454
+ for (const relativePath of installCopyPathsForBoard(latest.board_id)) {
324
455
  const sourcePath = resolve(sourceRoot.path, relativePath);
325
456
  if (!await pathExists(sourcePath)) {
326
457
  continue;
@@ -336,7 +467,7 @@ export async function installLocalToolchain(options = {}) {
336
467
  installedPaths.push(relativePath);
337
468
  }
338
469
  await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
339
- const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
470
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
340
471
  if (!validation.ok) {
341
472
  throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
342
473
  }
@@ -362,8 +493,9 @@ export async function installLocalToolchain(options = {}) {
362
493
  export async function validateLocalToolchain(input) {
363
494
  const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
364
495
  const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
496
+ const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
365
497
  const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
366
- const required = requiredLocalToolchainChecks(mode);
498
+ const required = requiredLocalToolchainChecks(mode, boardId);
367
499
  const checked_paths = [];
368
500
  for (const [label, relativePath] of required) {
369
501
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -381,13 +513,13 @@ export async function validateLocalToolchain(input) {
381
513
  platform: platform(),
382
514
  arch: arch()
383
515
  },
384
- board_id: DEFAULT_BOARD_ID,
516
+ board_id: boardId,
385
517
  release_root: resolvedRoot,
386
518
  checked_paths,
387
519
  missing_paths,
388
520
  notes: [
389
521
  "Local build commands require an Embed Labs auth token so local resource use remains account attributable.",
390
- `This validator checks the TaishanPi LLVM local support layout for install mode ${mode}.`
522
+ `This validator checks the ${boardId} local support layout for install mode ${mode}.`
391
523
  ]
392
524
  };
393
525
  }
@@ -401,7 +533,10 @@ function normalizeLocalToolchainInstallMode(mode) {
401
533
  }
402
534
  throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
403
535
  }
404
- function requiredLocalToolchainChecks(mode) {
536
+ function requiredLocalToolchainChecks(mode, boardId) {
537
+ if (isRp2350MonitorBoardId(boardId)) {
538
+ return requiredRp2350MonitorChecks(mode);
539
+ }
405
540
  const base = [
406
541
  ["release root", "."],
407
542
  ["Rockchip mkimage", "tools/mac/mkimage"],
@@ -411,8 +546,6 @@ function requiredLocalToolchainChecks(mode) {
411
546
  ["boot resource image", "boot-workspace/out/resource.img"],
412
547
  ["boot image", "boot-workspace/out/boot.img"],
413
548
  ["boot DTB", "boot-workspace/out/tspi-rk3566-user-v10-linux.dtb"],
414
- ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
415
- ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
416
549
  ["package metadata", "meta"]
417
550
  ];
418
551
  const compile = [
@@ -452,6 +585,25 @@ function requiredLocalToolchainChecks(mode) {
452
585
  }
453
586
  return [...base, ...compile, ...qt, ...images, ...full];
454
587
  }
588
+ function requiredRp2350MonitorChecks(mode) {
589
+ const runtime = [
590
+ ["release root", "."],
591
+ ["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
592
+ ["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
593
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
594
+ ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
595
+ ["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
596
+ ["package metadata", "meta"]
597
+ ];
598
+ const firmware = [
599
+ ["RP2350 Monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
600
+ ["RP2350 Monitor firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
601
+ ];
602
+ if (mode === "firmware" || mode === "full") {
603
+ return [...runtime, ...firmware];
604
+ }
605
+ return runtime;
606
+ }
455
607
  export async function compileTaishanPiSingleFile(options) {
456
608
  assertAuthenticated(options.auth);
457
609
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
@@ -521,15 +673,14 @@ export async function buildTaishanPiQtSmoke(options) {
521
673
  }
522
674
  async function loadLocalToolchainMetadata(metadataRoot, channelName) {
523
675
  const explicitRoot = metadataRoot || process.env.EMBEDLABS_METADATA_ROOT?.trim();
524
- const candidateRoot = explicitRoot || (await pathExists(DEFAULT_METADATA_ROOT) ? DEFAULT_METADATA_ROOT : undefined);
525
- if (!candidateRoot) {
676
+ if (!explicitRoot) {
526
677
  return {
527
678
  channel: BUILT_IN_CHANNEL,
528
679
  manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
529
680
  metadataRoot: undefined
530
681
  };
531
682
  }
532
- const root = resolve(candidateRoot);
683
+ const root = resolve(explicitRoot);
533
684
  const channelPath = join(root, "channels", channelName, "index.json");
534
685
  const channel = JSON.parse(await readFile(channelPath, "utf8"));
535
686
  if (channel.schema !== "embedlabs.channel.v1") {
@@ -552,17 +703,26 @@ async function loadLocalToolchainMetadata(metadataRoot, channelName) {
552
703
  return { channel, manifests, metadataRoot: root };
553
704
  }
554
705
  async function resolveLocalToolchainDownloadPlan(input) {
706
+ const requestedBoardId = normalizeBoardId(input.boardId);
707
+ const compatibleBoardIds = compatibleDownloadBoardIds(requestedBoardId);
555
708
  const channelUrl = downloadChannelUrl(input.channel);
556
709
  const channel = await fetchJson(channelUrl);
557
710
  if (channel.schema !== "embedlabs.download-channel.v1") {
558
711
  throw new Error(`Unexpected download channel schema ${channel.schema}.`);
559
712
  }
560
- const entry = (channel.packages ?? []).find((item) => {
561
- return item.board_id === input.boardId
562
- && item.host === input.host
563
- && item.toolchain === input.toolchain
564
- && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
565
- });
713
+ const entries = channel.packages ?? [];
714
+ let entry;
715
+ for (const boardId of compatibleBoardIds) {
716
+ entry = entries.find((item) => {
717
+ return normalizeBoardId(item.board_id ?? "") === boardId
718
+ && item.host === input.host
719
+ && item.toolchain === input.toolchain
720
+ && (item.kind === undefined || item.kind === "toolchain-archive" || item.kind === "board-support-archive");
721
+ });
722
+ if (entry) {
723
+ break;
724
+ }
725
+ }
566
726
  if (!entry?.manifest) {
567
727
  return undefined;
568
728
  }
@@ -571,8 +731,9 @@ async function resolveLocalToolchainDownloadPlan(input) {
571
731
  if (manifest.id !== entry.id || manifest.version !== entry.version) {
572
732
  throw new Error(`Download manifest mismatch for ${entry.id}@${entry.version}.`);
573
733
  }
574
- if (manifest.board_id !== input.boardId || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
575
- throw new Error(`Download manifest does not match requested ${input.boardId}/${input.host}/${input.toolchain}.`);
734
+ const manifestBoardId = normalizeBoardId(manifest.board_id ?? "");
735
+ if (!compatibleBoardIds.includes(manifestBoardId) || manifest.host !== input.host || manifest.toolchain !== input.toolchain) {
736
+ throw new Error(`Download manifest does not match requested ${requestedBoardId}/${input.host}/${input.toolchain}.`);
576
737
  }
577
738
  if ((!manifest.archive?.file || !manifest.archive.sha256 || !Number.isFinite(manifest.archive.size_bytes))
578
739
  && (!Array.isArray(manifest.components) || manifest.components.length === 0)) {
@@ -589,7 +750,7 @@ async function resolveLocalToolchainDownloadPlan(input) {
589
750
  size_bytes: Number.isFinite(mirror.size_bytes) ? mirror.size_bytes : manifest.archive?.size_bytes
590
751
  })), manifest.download_policy?.preferred_order)
591
752
  : [];
592
- const components = (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl));
753
+ const components = downloadComponentsForBoard(requestedBoardId, (manifest.components ?? []).map((component) => normalizeDownloadComponent(component, manifest, manifestUrl)));
593
754
  const first = mirrors[0];
594
755
  if (!first && components.length === 0) {
595
756
  return undefined;
@@ -599,7 +760,7 @@ async function resolveLocalToolchainDownloadPlan(input) {
599
760
  manifest_url: manifestUrl,
600
761
  package_id: manifest.id,
601
762
  version: manifest.version,
602
- board_id: input.boardId,
763
+ board_id: requestedBoardId,
603
764
  host: input.host,
604
765
  toolchain: input.toolchain,
605
766
  source_url: first?.url,
@@ -706,10 +867,27 @@ function resolvePackageRefs(packageId, channel, manifests, seen = new Set()) {
706
867
  return refs;
707
868
  }
708
869
  function boardPackageIdFor(boardId) {
709
- if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
870
+ const normalized = normalizeBoardId(boardId);
871
+ if (normalized === DEFAULT_BOARD_ID || normalized === "taishanpi" || normalized === "taishanpi-1m-rk3566") {
710
872
  return "embedlabs.board.taishanpi.1m-rk3566";
711
873
  }
712
- if (boardId.startsWith("embedlabs.board.")) {
874
+ if (normalized === COLOREASYPICO2_RP2350_BOARD_ID
875
+ || normalized === "coloreasypico2"
876
+ || normalized === "color-easy-pico2"
877
+ || normalized === "color-easy-pico-2"
878
+ || normalized === "color-easy-pico-2-rp2350-monitor"
879
+ || normalized === "ce-pico2") {
880
+ return "embedlabs.board.coloreasypico2.rp2350-monitor";
881
+ }
882
+ if (normalized === PICO2W_RP2350_BOARD_ID
883
+ || normalized === "pico2w"
884
+ || normalized === "pico-2-w"
885
+ || normalized === "pico2"
886
+ || normalized === "rp2350"
887
+ || normalized === "rp2350-monitor") {
888
+ return "embedlabs.board.pico2w.rp2350-monitor";
889
+ }
890
+ if (normalized.startsWith("embedlabs.board.")) {
713
891
  return boardId;
714
892
  }
715
893
  throw new Error(`Unsupported local toolchain board ${boardId}.`);
@@ -738,6 +916,44 @@ function boardIdForPackageManifest(manifest) {
738
916
  function normalizeBoardId(boardId) {
739
917
  return boardId.trim().toLowerCase().replaceAll("_", "-");
740
918
  }
919
+ function isRp2350MonitorBoardId(boardId) {
920
+ const normalized = normalizeBoardId(boardId);
921
+ return normalized === PICO2W_RP2350_BOARD_ID
922
+ || normalized === COLOREASYPICO2_RP2350_BOARD_ID
923
+ || normalized === "pico2w"
924
+ || normalized === "pico-2-w"
925
+ || normalized === "pico2"
926
+ || normalized === "rp2350"
927
+ || normalized === "rp2350-monitor"
928
+ || normalized === "coloreasypico2"
929
+ || normalized === "color-easy-pico2"
930
+ || normalized === "color-easy-pico-2"
931
+ || normalized === "ce-pico2";
932
+ }
933
+ function compatibleDownloadBoardIds(boardId) {
934
+ const normalized = normalizeBoardId(boardId);
935
+ if (normalized === COLOREASYPICO2_RP2350_BOARD_ID) {
936
+ return [COLOREASYPICO2_RP2350_BOARD_ID, PICO2W_RP2350_BOARD_ID];
937
+ }
938
+ return [normalized];
939
+ }
940
+ function downloadComponentsForBoard(boardId, components) {
941
+ const normalized = normalizeBoardId(boardId);
942
+ if (normalized === DEFAULT_BOARD_ID) {
943
+ return components.filter((component) => !isRp2350MonitorComponent(component));
944
+ }
945
+ return components;
946
+ }
947
+ function isRp2350MonitorComponent(component) {
948
+ const text = `${component.id} ${component.role ?? ""} ${component.archive.file}`.toLowerCase();
949
+ return text.includes("rp2350-monitor") || text.includes("pico2w-rp2350-monitor");
950
+ }
951
+ function compareVersionLike(left, right) {
952
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
953
+ }
954
+ function isRecord(value) {
955
+ return !!value && typeof value === "object" && !Array.isArray(value);
956
+ }
741
957
  function packageHostSupport(packages, manifests, host) {
742
958
  const unsupportedPackages = [];
743
959
  for (const item of packages) {
@@ -753,6 +969,9 @@ function packageHostSupport(packages, manifests, host) {
753
969
  }
754
970
  function environmentNotes(input) {
755
971
  const notes = [];
972
+ if (normalizeBoardId(input.boardId) === COLOREASYPICO2_RP2350_BOARD_ID) {
973
+ notes.push("ColorEasyPICO2 uses the same RP2350 Monitor runtime and UF2 firmware package; Wi-Fi operations are not declared for this no-Wi-Fi board.");
974
+ }
756
975
  if (input.status === "available") {
757
976
  notes.push("Environment is available but not installed on this computer.");
758
977
  }
@@ -767,6 +986,20 @@ function environmentNotes(input) {
767
986
  }
768
987
  return notes;
769
988
  }
989
+ function localToolchainInstallModesForDownload(download) {
990
+ if (!download?.components?.length) {
991
+ return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
992
+ }
993
+ const modes = new Set();
994
+ for (const component of download.components) {
995
+ for (const mode of component.install_modes ?? []) {
996
+ modes.add(mode);
997
+ }
998
+ }
999
+ return modes.size > 0
1000
+ ? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
1001
+ : [...LOCAL_TOOLCHAIN_INSTALL_MODES];
1002
+ }
770
1003
  function localToolchainEnvironmentComponent(component) {
771
1004
  return {
772
1005
  id: component.id,
@@ -798,7 +1031,14 @@ function localToolchainRegistryPath(installRoot) {
798
1031
  async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
799
1032
  const registryPath = localToolchainRegistryPath(installRoot);
800
1033
  await mkdir(dirname(registryPath), { recursive: true });
801
- await writeFile(registryPath, `${JSON.stringify({
1034
+ let existing = {};
1035
+ try {
1036
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
1037
+ }
1038
+ catch {
1039
+ existing = {};
1040
+ }
1041
+ const entry = {
802
1042
  installed: true,
803
1043
  board_id: latest.board_id,
804
1044
  version: latest.version,
@@ -810,6 +1050,19 @@ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, sour
810
1050
  source,
811
1051
  installed_components: source?.components,
812
1052
  updated_at: new Date().toISOString()
1053
+ };
1054
+ const environments = isRecord(existing.environments)
1055
+ ? { ...existing.environments }
1056
+ : {};
1057
+ environments[normalizeBoardId(latest.board_id)] = entry;
1058
+ const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
1059
+ && typeof existing.release_root === "string"
1060
+ && normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
1061
+ const topLevel = preserveTopLevel ? existing : entry;
1062
+ await writeFile(registryPath, `${JSON.stringify({
1063
+ ...topLevel,
1064
+ environments,
1065
+ updated_at: new Date().toISOString()
813
1066
  }, null, 2)}\n`, "utf8");
814
1067
  }
815
1068
  async function resolveLocalReleaseRoot(releaseRoot) {
@@ -963,6 +1216,16 @@ function selectedDownloadComponents(components, mode) {
963
1216
  return component.install_modes.includes(mode);
964
1217
  });
965
1218
  }
1219
+ function installCopyPathsForBoard(boardId) {
1220
+ if (normalizeBoardId(boardId) === DEFAULT_BOARD_ID) {
1221
+ return INSTALL_COPY_PATHS.filter((relativePath) => {
1222
+ const normalized = relativePath.toLowerCase();
1223
+ return normalized !== "toolkit-runtime/rp2350-monitor"
1224
+ && normalized !== "toolkit-runtime/rp2350-monitor/";
1225
+ });
1226
+ }
1227
+ return INSTALL_COPY_PATHS;
1228
+ }
966
1229
  async function downloadToolchainArchive(sourceUrl, installRoot, expected) {
967
1230
  const downloadsDir = join(installRoot, "cache", "downloads");
968
1231
  await mkdir(downloadsDir, { recursive: true });