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

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,8 @@ 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
14
  const DEFAULT_CHANNEL = "stable";
15
15
  const DEFAULT_DOWNLOAD_BASE_URL = "https://download.embedboard.com";
16
16
  const DOWNLOAD_REQUEST_TIMEOUT_MS = 12_000;
@@ -24,7 +24,9 @@ const BUILT_IN_CHANNEL = {
24
24
  { id: "embedlabs.tools.runtime.qtquick-live-preview", version: "1.0.31", manifest: "" },
25
25
  { id: "embedlabs.tools.runtime.rp2350-monitor", version: "1.0.31", manifest: "" },
26
26
  { id: "embedlabs.family.rk356x", version: "1.0.0", manifest: "" },
27
- { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" }
27
+ { id: "embedlabs.family.rp2350", version: "1.0.0", manifest: "" },
28
+ { id: "embedlabs.board.taishanpi.1m-rk3566", version: "1.0.31", manifest: "" },
29
+ { id: "embedlabs.board.pico2w.rp2350-monitor", version: "1.0.31", manifest: "" }
28
30
  ]
29
31
  };
30
32
  const BUILT_IN_MANIFESTS = {
@@ -80,6 +82,16 @@ const BUILT_IN_MANIFESTS = {
80
82
  { id: "embedlabs.tools.common.e2fsprogs", version: "^1.0.0" }
81
83
  ]
82
84
  },
85
+ "embedlabs.family.rp2350": {
86
+ schema: "embedlabs.package.v1",
87
+ id: "embedlabs.family.rp2350",
88
+ version: "1.0.0",
89
+ kind: "family",
90
+ family: "rp2350",
91
+ requires: [
92
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31" }
93
+ ]
94
+ },
83
95
  "embedlabs.board.taishanpi.1m-rk3566": {
84
96
  schema: "embedlabs.package.v1",
85
97
  id: "embedlabs.board.taishanpi.1m-rk3566",
@@ -97,6 +109,22 @@ const BUILT_IN_MANIFESTS = {
97
109
  { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["rp2350-monitor"] }
98
110
  ],
99
111
  build_modes: ["local-llvm"]
112
+ },
113
+ "embedlabs.board.pico2w.rp2350-monitor": {
114
+ schema: "embedlabs.package.v1",
115
+ id: "embedlabs.board.pico2w.rp2350-monitor",
116
+ version: "1.0.31",
117
+ kind: "board",
118
+ display_name: "Pico 2 W / RP2350 Monitor",
119
+ family: "rp2350",
120
+ board: "Pico 2 W",
121
+ variant: "RP2350 Monitor",
122
+ board_id: PICO2W_RP2350_BOARD_ID,
123
+ requires: [
124
+ { id: "embedlabs.family.rp2350", version: "^1.0.0" },
125
+ { id: "embedlabs.tools.runtime.rp2350-monitor", version: "^1.0.31", roles: ["hardware-control", "logic-analyzer", "debug-probe"] }
126
+ ],
127
+ build_modes: ["local-monitor"]
100
128
  }
101
129
  };
102
130
  const INSTALL_COPY_PATHS = [
@@ -119,7 +147,7 @@ const INSTALL_COPY_PATHS = [
119
147
  "support",
120
148
  "third_party"
121
149
  ];
122
- const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "compile", "qt", "full", "images"];
150
+ const LOCAL_TOOLCHAIN_INSTALL_MODES = ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"];
123
151
  export function defaultLocalReleaseRoot() {
124
152
  return process.env.EMBEDLABS_LOCAL_RELEASE_ROOT?.trim()
125
153
  || process.env.EMBEDLABS_RELEASE_ROOT?.trim()
@@ -134,12 +162,13 @@ export async function latestLocalToolchain(options = {}) {
134
162
  if (!board) {
135
163
  throw new Error(`No local toolchain board package found for ${boardId}.`);
136
164
  }
165
+ const canonicalBoardId = boardIdForPackageManifest(board);
137
166
  const packages = resolvePackageRefs(boardPackageId, channel, manifests);
138
167
  let download;
139
168
  let downloadError;
140
169
  try {
141
170
  download = await resolveLocalToolchainDownloadPlan({
142
- boardId,
171
+ boardId: canonicalBoardId,
143
172
  channel: channelName,
144
173
  host: hostId(),
145
174
  toolchain: "llvm"
@@ -149,7 +178,7 @@ export async function latestLocalToolchain(options = {}) {
149
178
  downloadError = error instanceof Error ? error.message : String(error);
150
179
  }
151
180
  return {
152
- board_id: boardId,
181
+ board_id: canonicalBoardId,
153
182
  channel: channel.channel,
154
183
  host: hostId(),
155
184
  version: download?.version ?? board.version,
@@ -164,10 +193,34 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
164
193
  const registryPath = localToolchainRegistryPath(root);
165
194
  try {
166
195
  const registry = JSON.parse(await readFile(registryPath, "utf8"));
196
+ const environments = registry.environments;
197
+ const boardInstall = environments?.[normalizeBoardId(boardId)];
198
+ if (boardInstall?.release_root) {
199
+ return {
200
+ installed: true,
201
+ board_id: typeof boardInstall.board_id === "string" ? boardInstall.board_id : boardId,
202
+ version: typeof boardInstall.version === "string" ? boardInstall.version : undefined,
203
+ mode: typeof boardInstall.mode === "string" ? boardInstall.mode : undefined,
204
+ release_root: boardInstall.release_root,
205
+ registry_path: registryPath,
206
+ install_root: root,
207
+ channel: typeof boardInstall.channel === "string" ? boardInstall.channel : undefined,
208
+ packages: Array.isArray(boardInstall.packages) ? boardInstall.packages : undefined
209
+ };
210
+ }
167
211
  const releaseRoot = typeof registry.release_root === "string" ? registry.release_root : undefined;
212
+ const registryBoardId = typeof registry.board_id === "string" ? registry.board_id : boardId;
213
+ if (releaseRoot && normalizeBoardId(registryBoardId) !== normalizeBoardId(boardId)) {
214
+ return {
215
+ installed: false,
216
+ board_id: boardId,
217
+ registry_path: registryPath,
218
+ install_root: root
219
+ };
220
+ }
168
221
  return {
169
222
  installed: !!releaseRoot,
170
- board_id: typeof registry.board_id === "string" ? registry.board_id : boardId,
223
+ board_id: registryBoardId,
171
224
  version: typeof registry.version === "string" ? registry.version : undefined,
172
225
  mode: typeof registry.mode === "string" ? registry.mode : undefined,
173
226
  release_root: releaseRoot,
@@ -186,6 +239,58 @@ export async function currentLocalToolchain(installRoot, boardId = DEFAULT_BOARD
186
239
  };
187
240
  }
188
241
  }
242
+ async function discoverInstalledLocalToolchains(installRoot, current) {
243
+ const installed = new Map();
244
+ if (current.installed && current.release_root) {
245
+ installed.set(normalizeBoardId(current.board_id), {
246
+ board_id: current.board_id,
247
+ version: current.version,
248
+ channel: current.channel,
249
+ mode: current.mode,
250
+ release_root: current.release_root
251
+ });
252
+ }
253
+ const toolchainsRoot = join(installRoot, "toolchains");
254
+ let boardEntries;
255
+ try {
256
+ boardEntries = await readdir(toolchainsRoot, { withFileTypes: true });
257
+ }
258
+ catch {
259
+ return installed;
260
+ }
261
+ for (const boardEntry of boardEntries) {
262
+ if (!boardEntry.isDirectory()) {
263
+ continue;
264
+ }
265
+ const boardId = normalizeBoardId(boardEntry.name);
266
+ if (installed.has(boardId)) {
267
+ continue;
268
+ }
269
+ const boardRoot = join(toolchainsRoot, boardEntry.name);
270
+ let versionEntries;
271
+ try {
272
+ versionEntries = await readdir(boardRoot, { withFileTypes: true });
273
+ }
274
+ catch {
275
+ continue;
276
+ }
277
+ const versions = versionEntries
278
+ .filter((entry) => entry.isDirectory())
279
+ .map((entry) => entry.name)
280
+ .sort(compareVersionLike)
281
+ .reverse();
282
+ const version = versions[0];
283
+ if (!version) {
284
+ continue;
285
+ }
286
+ installed.set(boardId, {
287
+ board_id: boardId,
288
+ version,
289
+ release_root: join(boardRoot, version)
290
+ });
291
+ }
292
+ return installed;
293
+ }
189
294
  export async function listLocalToolchainEnvironments(options = {}) {
190
295
  const channelName = options.channel ?? DEFAULT_CHANNEL;
191
296
  const host = hostId();
@@ -193,6 +298,7 @@ export async function listLocalToolchainEnvironments(options = {}) {
193
298
  const registryPath = localToolchainRegistryPath(installRoot);
194
299
  const { channel, manifests, metadataRoot } = await loadLocalToolchainMetadata(options.metadataRoot, channelName);
195
300
  const current = await currentLocalToolchain(installRoot);
301
+ const installedByBoard = await discoverInstalledLocalToolchains(installRoot, current);
196
302
  const boardManifests = [...manifests.values()]
197
303
  .filter((manifest) => manifest.kind === "board")
198
304
  .filter((manifest) => {
@@ -224,12 +330,16 @@ export async function listLocalToolchainEnvironments(options = {}) {
224
330
  downloadError = error instanceof Error ? error.message : String(error);
225
331
  }
226
332
  const latestVersion = download?.version ?? board.version;
227
- const installed = current.installed && normalizeBoardId(current.board_id) === normalizeBoardId(boardId)
333
+ const currentForBoard = await currentLocalToolchain(installRoot, boardId);
334
+ const installedCandidate = currentForBoard.installed
335
+ ? currentForBoard
336
+ : installedByBoard.get(normalizeBoardId(boardId));
337
+ const installed = installedCandidate
228
338
  ? {
229
- version: current.version,
230
- channel: current.channel,
231
- mode: current.mode,
232
- release_root: current.release_root
339
+ version: installedCandidate.version,
340
+ channel: installedCandidate.channel,
341
+ mode: installedCandidate.mode,
342
+ release_root: installedCandidate.release_root
233
343
  }
234
344
  : undefined;
235
345
  const updateAvailable = !!installed?.version && installed.version !== latestVersion;
@@ -241,6 +351,7 @@ export async function listLocalToolchainEnvironments(options = {}) {
241
351
  ? "installed"
242
352
  : "available";
243
353
  const mode = download?.default_mode ?? "qt";
354
+ const installModes = localToolchainInstallModesForDownload(download);
244
355
  environments.push({
245
356
  board_id: boardId,
246
357
  package_id: board.id,
@@ -252,7 +363,7 @@ export async function listLocalToolchainEnvironments(options = {}) {
252
363
  status,
253
364
  supported_host: hostSupport.supported,
254
365
  unsupported_packages: hostSupport.unsupportedPackages,
255
- install_modes: [...LOCAL_TOOLCHAIN_INSTALL_MODES],
366
+ install_modes: installModes,
256
367
  installed,
257
368
  latest: {
258
369
  version: latestVersion,
@@ -269,13 +380,17 @@ export async function listLocalToolchainEnvironments(options = {}) {
269
380
  notes: environmentNotes({ status, downloadError, unsupportedPackages: hostSupport.unsupportedPackages })
270
381
  });
271
382
  }
383
+ const filteredEnvironments = options.installedOnly
384
+ ? environments.filter((environment) => !!environment.installed)
385
+ : environments;
272
386
  return {
273
387
  host,
274
388
  channel: channel.channel,
389
+ metadata_source: metadataRoot ? "local_override" : "built_in",
275
390
  metadata_root: metadataRoot,
276
391
  install_root: installRoot,
277
392
  registry_path: registryPath,
278
- environments
393
+ environments: filteredEnvironments
279
394
  };
280
395
  }
281
396
  export async function installLocalToolchain(options = {}) {
@@ -284,7 +399,7 @@ export async function installLocalToolchain(options = {}) {
284
399
  const releaseRoot = resolve(installRoot, "toolchains", latest.board_id, latest.version);
285
400
  const installMode = normalizeLocalToolchainInstallMode(options.mode ?? latest.download?.default_mode);
286
401
  if (await pathExists(releaseRoot) && !options.force) {
287
- const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
402
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
288
403
  if (!validation.ok) {
289
404
  if (latest.download?.components?.length) {
290
405
  // Component installs can upgrade an existing lower-mode install by overlaying
@@ -336,7 +451,7 @@ export async function installLocalToolchain(options = {}) {
336
451
  installedPaths.push(relativePath);
337
452
  }
338
453
  await writeCurrentRegistry(installRoot, latest, releaseRoot, installMode, sourceRoot.source);
339
- const validation = await validateLocalToolchain({ releaseRoot, mode: installMode });
454
+ const validation = await validateLocalToolchain({ releaseRoot, mode: installMode, boardId: latest.board_id });
340
455
  if (!validation.ok) {
341
456
  throw new Error(`Installed local toolchain is incomplete: ${validation.missing_paths.join(", ")}`);
342
457
  }
@@ -362,8 +477,9 @@ export async function installLocalToolchain(options = {}) {
362
477
  export async function validateLocalToolchain(input) {
363
478
  const releaseRoot = typeof input === "string" ? input : input?.releaseRoot;
364
479
  const mode = normalizeLocalToolchainInstallMode(typeof input === "string" ? undefined : input?.mode);
480
+ const boardId = normalizeBoardId(typeof input === "string" ? DEFAULT_BOARD_ID : input?.boardId ?? DEFAULT_BOARD_ID);
365
481
  const resolvedRoot = await resolveLocalReleaseRoot(releaseRoot);
366
- const required = requiredLocalToolchainChecks(mode);
482
+ const required = requiredLocalToolchainChecks(mode, boardId);
367
483
  const checked_paths = [];
368
484
  for (const [label, relativePath] of required) {
369
485
  const absolutePath = resolve(resolvedRoot, relativePath);
@@ -381,13 +497,13 @@ export async function validateLocalToolchain(input) {
381
497
  platform: platform(),
382
498
  arch: arch()
383
499
  },
384
- board_id: DEFAULT_BOARD_ID,
500
+ board_id: boardId,
385
501
  release_root: resolvedRoot,
386
502
  checked_paths,
387
503
  missing_paths,
388
504
  notes: [
389
505
  "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}.`
506
+ `This validator checks the ${boardId} local support layout for install mode ${mode}.`
391
507
  ]
392
508
  };
393
509
  }
@@ -401,7 +517,10 @@ function normalizeLocalToolchainInstallMode(mode) {
401
517
  }
402
518
  throw new Error(`Unsupported local toolchain install mode ${normalized}; expected ${LOCAL_TOOLCHAIN_INSTALL_MODES.join(", ")}.`);
403
519
  }
404
- function requiredLocalToolchainChecks(mode) {
520
+ function requiredLocalToolchainChecks(mode, boardId) {
521
+ if (boardId === PICO2W_RP2350_BOARD_ID || boardId === "rp2350" || boardId === "rp2350-monitor") {
522
+ return requiredRp2350MonitorChecks(mode);
523
+ }
405
524
  const base = [
406
525
  ["release root", "."],
407
526
  ["Rockchip mkimage", "tools/mac/mkimage"],
@@ -452,6 +571,25 @@ function requiredLocalToolchainChecks(mode) {
452
571
  }
453
572
  return [...base, ...compile, ...qt, ...images, ...full];
454
573
  }
574
+ function requiredRp2350MonitorChecks(mode) {
575
+ const runtime = [
576
+ ["release root", "."],
577
+ ["RP2350 Monitor UI", "toolkit-runtime/rp2350-monitor/ui/index.html"],
578
+ ["RP2350 Monitor bridge", "toolkit-runtime/rp2350-monitor/ui/bridge/rpmon_bridge.py"],
579
+ ["RP2350 Monitor CLI", "toolkit-runtime/rp2350-monitor/tools/rpmon_cli.py"],
580
+ ["RP2350 Monitor logic analyzer", "toolkit-runtime/rp2350-monitor/ui/bin/embed-labs-logic-analyzer"],
581
+ ["RP2350 Monitor AI operation contract", "toolkit-runtime/rp2350-monitor/ui/docs/ai-operation-contract.md"],
582
+ ["package metadata", "meta"]
583
+ ];
584
+ const firmware = [
585
+ ["Pico 2 W monitor UF2", "toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2"],
586
+ ["Pico 2 W firmware source", "toolkit-runtime/rp2350-monitor/firmware/src/main.cpp"]
587
+ ];
588
+ if (mode === "firmware" || mode === "full") {
589
+ return [...runtime, ...firmware];
590
+ }
591
+ return runtime;
592
+ }
455
593
  export async function compileTaishanPiSingleFile(options) {
456
594
  assertAuthenticated(options.auth);
457
595
  const releaseRoot = await resolveLocalReleaseRoot(options.releaseRoot);
@@ -521,15 +659,14 @@ export async function buildTaishanPiQtSmoke(options) {
521
659
  }
522
660
  async function loadLocalToolchainMetadata(metadataRoot, channelName) {
523
661
  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) {
662
+ if (!explicitRoot) {
526
663
  return {
527
664
  channel: BUILT_IN_CHANNEL,
528
665
  manifests: new Map(Object.entries(BUILT_IN_MANIFESTS)),
529
666
  metadataRoot: undefined
530
667
  };
531
668
  }
532
- const root = resolve(candidateRoot);
669
+ const root = resolve(explicitRoot);
533
670
  const channelPath = join(root, "channels", channelName, "index.json");
534
671
  const channel = JSON.parse(await readFile(channelPath, "utf8"));
535
672
  if (channel.schema !== "embedlabs.channel.v1") {
@@ -709,6 +846,14 @@ function boardPackageIdFor(boardId) {
709
846
  if (boardId === DEFAULT_BOARD_ID || boardId === "taishanpi" || boardId === "taishanpi-1m-rk3566") {
710
847
  return "embedlabs.board.taishanpi.1m-rk3566";
711
848
  }
849
+ if (boardId === PICO2W_RP2350_BOARD_ID
850
+ || boardId === "pico2w"
851
+ || boardId === "pico-2-w"
852
+ || boardId === "pico2"
853
+ || boardId === "rp2350"
854
+ || boardId === "rp2350-monitor") {
855
+ return "embedlabs.board.pico2w.rp2350-monitor";
856
+ }
712
857
  if (boardId.startsWith("embedlabs.board.")) {
713
858
  return boardId;
714
859
  }
@@ -738,6 +883,12 @@ function boardIdForPackageManifest(manifest) {
738
883
  function normalizeBoardId(boardId) {
739
884
  return boardId.trim().toLowerCase().replaceAll("_", "-");
740
885
  }
886
+ function compareVersionLike(left, right) {
887
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "base" });
888
+ }
889
+ function isRecord(value) {
890
+ return !!value && typeof value === "object" && !Array.isArray(value);
891
+ }
741
892
  function packageHostSupport(packages, manifests, host) {
742
893
  const unsupportedPackages = [];
743
894
  for (const item of packages) {
@@ -767,6 +918,20 @@ function environmentNotes(input) {
767
918
  }
768
919
  return notes;
769
920
  }
921
+ function localToolchainInstallModesForDownload(download) {
922
+ if (!download?.components?.length) {
923
+ return [...LOCAL_TOOLCHAIN_INSTALL_MODES];
924
+ }
925
+ const modes = new Set();
926
+ for (const component of download.components) {
927
+ for (const mode of component.install_modes ?? []) {
928
+ modes.add(mode);
929
+ }
930
+ }
931
+ return modes.size > 0
932
+ ? [...modes].filter((mode) => LOCAL_TOOLCHAIN_INSTALL_MODES.includes(mode))
933
+ : [...LOCAL_TOOLCHAIN_INSTALL_MODES];
934
+ }
770
935
  function localToolchainEnvironmentComponent(component) {
771
936
  return {
772
937
  id: component.id,
@@ -798,7 +963,14 @@ function localToolchainRegistryPath(installRoot) {
798
963
  async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, source) {
799
964
  const registryPath = localToolchainRegistryPath(installRoot);
800
965
  await mkdir(dirname(registryPath), { recursive: true });
801
- await writeFile(registryPath, `${JSON.stringify({
966
+ let existing = {};
967
+ try {
968
+ existing = JSON.parse(await readFile(registryPath, "utf8"));
969
+ }
970
+ catch {
971
+ existing = {};
972
+ }
973
+ const entry = {
802
974
  installed: true,
803
975
  board_id: latest.board_id,
804
976
  version: latest.version,
@@ -810,6 +982,19 @@ async function writeCurrentRegistry(installRoot, latest, releaseRoot, mode, sour
810
982
  source,
811
983
  installed_components: source?.components,
812
984
  updated_at: new Date().toISOString()
985
+ };
986
+ const environments = isRecord(existing.environments)
987
+ ? { ...existing.environments }
988
+ : {};
989
+ environments[normalizeBoardId(latest.board_id)] = entry;
990
+ const preserveTopLevel = latest.board_id !== DEFAULT_BOARD_ID
991
+ && typeof existing.release_root === "string"
992
+ && normalizeBoardId(String(existing.board_id ?? DEFAULT_BOARD_ID)) === DEFAULT_BOARD_ID;
993
+ const topLevel = preserveTopLevel ? existing : entry;
994
+ await writeFile(registryPath, `${JSON.stringify({
995
+ ...topLevel,
996
+ environments,
997
+ updated_at: new Date().toISOString()
813
998
  }, null, 2)}\n`, "utf8");
814
999
  }
815
1000
  async function resolveLocalReleaseRoot(releaseRoot) {