@app-vitals/agent-runtime 1.4.0 → 1.6.0

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.
package/boot.ts CHANGED
@@ -523,13 +523,22 @@ export async function startAgent(
523
523
  });
524
524
 
525
525
  // Install agent-specific plugins from bundle (non-fatal)
526
- if (bundle.plugins?.length) {
527
- await installPlugins(undefined, undefined, bundle.plugins).catch(
528
- (err) =>
529
- console.warn(
530
- "[config-sync] agent plugin install failed (non-fatal):",
531
- (err as Error).message,
532
- ),
526
+ if (bundle.plugins?.length || bundle.marketplaces?.length) {
527
+ await installPlugins(
528
+ undefined,
529
+ undefined,
530
+ bundle.plugins,
531
+ undefined,
532
+ undefined,
533
+ undefined,
534
+ undefined,
535
+ undefined,
536
+ bundle.marketplaces,
537
+ ).catch((err) =>
538
+ console.warn(
539
+ "[config-sync] agent plugin install failed (non-fatal):",
540
+ (err as Error).message,
541
+ ),
533
542
  );
534
543
  }
535
544
 
package/entrypoint.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  import { join } from "node:path";
21
21
  import type { AgentPlugin } from "@app-vitals/agent-admin";
22
22
  import type { AgentConfigClient } from "./agent-config-client.ts";
23
+ import type { RemoteMarketplace } from "./setup.ts";
23
24
 
24
25
  // ─── Types ────────────────────────────────────────────────────────────────────
25
26
 
@@ -43,6 +44,12 @@ export interface EntrypointDeps {
43
44
  execFn?: ExecFn,
44
45
  cwd?: string,
45
46
  plugins?: AgentPlugin[],
47
+ shipwrightRepoRootOverride?: string,
48
+ pluginManifestPath?: string,
49
+ extraMarketplaceDirs?: string[],
50
+ extraMarketplacesRoot?: string,
51
+ env?: Record<string, string | undefined>,
52
+ remoteMarketplaces?: RemoteMarketplace[],
46
53
  ) => Promise<void>;
47
54
  spawnAgentServer: (cmd: string, args: string[]) => void;
48
55
  exit: (code: number) => void;
@@ -130,7 +137,17 @@ export async function runEntrypoint(deps: EntrypointDeps): Promise<void> {
130
137
  await runMiseStartup(agentHome);
131
138
 
132
139
  // Step 7: Install plugins
133
- await installPlugins(undefined, undefined, config.plugins);
140
+ await installPlugins(
141
+ undefined,
142
+ undefined,
143
+ config.plugins,
144
+ undefined,
145
+ undefined,
146
+ undefined,
147
+ undefined,
148
+ undefined,
149
+ config.marketplaces,
150
+ );
134
151
 
135
152
  // Step 8: Spawn agent server
136
153
  spawnAgentServer("bun", ["run", join(import.meta.dir, "..", "index.ts")]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-runtime",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/app-vitals/agent.git",
@@ -40,8 +40,8 @@
40
40
  "./workspace-paths": "./workspace-paths.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@app-vitals/agent-admin": "1.4.0",
44
- "@app-vitals/agent-lib": "1.4.0",
43
+ "@app-vitals/agent-admin": "1.6.0",
44
+ "@app-vitals/agent-lib": "1.6.0",
45
45
  "@octokit/auth-app": "^8.2.0",
46
46
  "@sentry/bun": "^10.63.0",
47
47
  "@slack/bolt": "^5.0.0",
@@ -1590,3 +1590,239 @@ describe("installPlugins stale shipwright marketplace healing", () => {
1590
1590
  expect(calls).toEqual([["plugin", "marketplace", "add", root]]);
1591
1591
  });
1592
1592
  });
1593
+
1594
+ // ---------------------------------------------------------------------------
1595
+ // installPlugins — remote marketplaces (PMK-1.3)
1596
+ // ---------------------------------------------------------------------------
1597
+
1598
+ describe("installPlugins — remote marketplaces", () => {
1599
+ const ROW = { name: "mkt", source: "owner/mkt", ref: "v1" };
1600
+ const NO_REPO = () => join(testHome, "no-repo");
1601
+ const registryPath = () => join(testHome, "known_marketplaces.json");
1602
+ const manifestPath = () => join(testHome, "installed_plugins.json");
1603
+
1604
+ const writeRegistry = (registry: unknown) => {
1605
+ mkdirSync(testHome, { recursive: true });
1606
+ writeFileSync(registryPath(), JSON.stringify(registry));
1607
+ };
1608
+
1609
+ // A marketplace dir the registry can point at.
1610
+ const installDir = (withManifest: boolean): string => {
1611
+ const dir = join(testHome, "marketplaces-cache", "mkt");
1612
+ mkdirSync(join(dir, ".claude-plugin"), { recursive: true });
1613
+ if (withManifest) {
1614
+ writeFileSync(
1615
+ join(dir, ".claude-plugin", "marketplace.json"),
1616
+ JSON.stringify({ name: "mkt" }),
1617
+ );
1618
+ }
1619
+ return dir;
1620
+ };
1621
+
1622
+ const githubEntry = (
1623
+ repo: string,
1624
+ ref: string | undefined,
1625
+ installLocation: string,
1626
+ ) => ({
1627
+ source: { source: "github", repo, ...(ref ? { ref } : {}) },
1628
+ installLocation,
1629
+ lastUpdated: "2026-11-09T00:00:00.000Z",
1630
+ });
1631
+
1632
+ const run = async (
1633
+ exec: (
1634
+ cmd: string,
1635
+ args: string[],
1636
+ opts: { cwd: string },
1637
+ ) => Promise<{ stdout: string; exitCode: number }>,
1638
+ rows: Array<{ name: string; source: string; ref?: string | null }>,
1639
+ extraDirs: string[] = NO_EXTRA_MARKETPLACES,
1640
+ plugins: AgentPlugin[] = [],
1641
+ ) =>
1642
+ installPlugins(
1643
+ exec,
1644
+ testHome,
1645
+ plugins,
1646
+ NO_REPO(),
1647
+ manifestPath(),
1648
+ extraDirs,
1649
+ undefined,
1650
+ {},
1651
+ rows,
1652
+ );
1653
+
1654
+ const recorder = (exitFor: (args: string[]) => number = () => 0) => {
1655
+ const calls: string[][] = [];
1656
+ const exec = async (_cmd: string, args: string[]) => {
1657
+ calls.push(args);
1658
+ return { stdout: "", exitCode: exitFor(args) };
1659
+ };
1660
+ return { calls, exec };
1661
+ };
1662
+
1663
+ it("with no rows the call sequence is unchanged", async () => {
1664
+ const { calls, exec } = recorder();
1665
+ await run(exec, [], NO_EXTRA_MARKETPLACES, [SHIPWRIGHT_ROW]);
1666
+ expect(calls).toEqual([
1667
+ ["plugin", "install", "shipwright@shipwright"],
1668
+ ["plugin", "update", "shipwright@shipwright"],
1669
+ ]);
1670
+ });
1671
+
1672
+ it("fresh home: baked adds, remote add <source>#<ref>, update, then plugins", async () => {
1673
+ const baked = join(testHome, "baked", "baked-mkt");
1674
+ const { calls, exec } = recorder();
1675
+ await run(exec, [ROW], [baked], [SHIPWRIGHT_ROW]);
1676
+ expect(calls).toEqual([
1677
+ ["plugin", "marketplace", "add", baked],
1678
+ ["plugin", "marketplace", "add", "owner/mkt#v1"],
1679
+ ["plugin", "marketplace", "update", "mkt"],
1680
+ ["plugin", "install", "shipwright@shipwright"],
1681
+ ["plugin", "update", "shipwright@shipwright"],
1682
+ ]);
1683
+ });
1684
+
1685
+ it("adds without a ref suffix when the row has no ref", async () => {
1686
+ const { calls, exec } = recorder();
1687
+ await run(exec, [{ name: "mkt", source: "owner/mkt", ref: null }]);
1688
+ expect(calls[0]).toEqual(["plugin", "marketplace", "add", "owner/mkt"]);
1689
+ });
1690
+
1691
+ it("same source and ref: only update", async () => {
1692
+ writeRegistry({ mkt: githubEntry("owner/mkt", "v1", installDir(true)) });
1693
+ const { calls, exec } = recorder();
1694
+ await run(exec, [ROW]);
1695
+ expect(calls).toEqual([["plugin", "marketplace", "update", "mkt"]]);
1696
+ });
1697
+
1698
+ it("different ref: remove, add, update", async () => {
1699
+ writeRegistry({ mkt: githubEntry("owner/mkt", "v0", installDir(true)) });
1700
+ const { calls, exec } = recorder();
1701
+ await run(exec, [ROW]);
1702
+ expect(calls).toEqual([
1703
+ ["plugin", "marketplace", "remove", "mkt"],
1704
+ ["plugin", "marketplace", "add", "owner/mkt#v1"],
1705
+ ["plugin", "marketplace", "update", "mkt"],
1706
+ ]);
1707
+ });
1708
+
1709
+ it("different source: remove, add, update", async () => {
1710
+ writeRegistry({ mkt: githubEntry("other/mkt", "v1", installDir(true)) });
1711
+ const { calls, exec } = recorder();
1712
+ await run(exec, [ROW]);
1713
+ expect(calls.map((c) => c[2])).toEqual(["remove", "add", "update"]);
1714
+ });
1715
+
1716
+ it("installLocation without marketplace.json: remove, add, update", async () => {
1717
+ writeRegistry({ mkt: githubEntry("owner/mkt", "v1", installDir(false)) });
1718
+ const { calls, exec } = recorder();
1719
+ await run(exec, [ROW]);
1720
+ expect(calls.map((c) => c[2])).toEqual(["remove", "add", "update"]);
1721
+ });
1722
+
1723
+ it("a git URL source matches its registered form", async () => {
1724
+ writeRegistry({
1725
+ mkt: {
1726
+ source: {
1727
+ source: "git",
1728
+ url: "https://github.com/Owner/mkt.git",
1729
+ ref: "v1",
1730
+ },
1731
+ installLocation: installDir(true),
1732
+ },
1733
+ });
1734
+ const { calls, exec } = recorder();
1735
+ await run(exec, [{ ...ROW, source: "https://github.com/owner/mkt" }]);
1736
+ expect(calls).toEqual([["plugin", "marketplace", "update", "mkt"]]);
1737
+ });
1738
+
1739
+ it("a baked dir of the same name suppresses the row with a warning", async () => {
1740
+ const baked = join(testHome, "baked", "mkt");
1741
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
1742
+ const { calls, exec } = recorder();
1743
+ await run(exec, [ROW], [baked]);
1744
+ expect(calls).toEqual([["plugin", "marketplace", "add", baked]]);
1745
+ expect(warn.mock.calls.flat().join("\n")).toContain("baked marketplace");
1746
+ warn.mockRestore();
1747
+ });
1748
+
1749
+ it("warns on a manifest name mismatch", async () => {
1750
+ // The CLI registers under the manifest name, not the row name.
1751
+ writeRegistry({
1752
+ actual: githubEntry("owner/mkt", "v1", installDir(true)),
1753
+ });
1754
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
1755
+ const { exec } = recorder();
1756
+ await run(exec, [ROW]);
1757
+ expect(warn.mock.calls.flat().join("\n")).toContain("named actual");
1758
+ warn.mockRestore();
1759
+ });
1760
+
1761
+ it("failed add, remove and update warn and boot continues", async () => {
1762
+ writeRegistry({ mkt: githubEntry("owner/mkt", "v0", installDir(true)) });
1763
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
1764
+ const { calls, exec } = recorder((args) =>
1765
+ args[0] === "plugin" && args[1] === "marketplace" ? 1 : 0,
1766
+ );
1767
+ await run(exec, [ROW], NO_EXTRA_MARKETPLACES, [SHIPWRIGHT_ROW]);
1768
+ const text = warn.mock.calls.flat().join("\n");
1769
+ expect(text).toContain("marketplace remove mkt exited 1");
1770
+ expect(text).toContain("marketplace add owner/mkt#v1 exited 1");
1771
+ expect(text).toContain("marketplace update mkt exited 1");
1772
+ expect(calls.at(-1)).toEqual(["plugin", "update", "shipwright@shipwright"]);
1773
+ warn.mockRestore();
1774
+ });
1775
+
1776
+ it("never removes registrations the agent does not manage", async () => {
1777
+ writeRegistry({
1778
+ unmanaged: githubEntry("x/y", undefined, installDir(false)),
1779
+ mkt: githubEntry("owner/mkt", "v1", installDir(true)),
1780
+ });
1781
+ const { calls, exec } = recorder();
1782
+ await run(exec, [ROW]);
1783
+ expect(calls.some((c) => c.includes("unmanaged"))).toBe(false);
1784
+ expect(calls.some((c) => c[2] === "remove")).toBe(false);
1785
+ });
1786
+
1787
+ it("keeps the baked stale-registration heal working alongside rows", async () => {
1788
+ const baked = join(testHome, "baked", "heal-mkt");
1789
+ writeRegistry({
1790
+ "heal-mkt": {
1791
+ source: { source: "directory", path: "/nonexistent-app" },
1792
+ installLocation: "/nonexistent-app",
1793
+ },
1794
+ });
1795
+ const { calls, exec } = recorder();
1796
+ await run(exec, [ROW], [baked]);
1797
+ expect(calls.slice(0, 2)).toEqual([
1798
+ ["plugin", "marketplace", "remove", "heal-mkt"],
1799
+ ["plugin", "marketplace", "add", baked],
1800
+ ]);
1801
+ });
1802
+
1803
+ it("two consecutive boots are idempotent", async () => {
1804
+ // Fake CLI that persists registrations the way `claude` does.
1805
+ const cliExec = async (_cmd: string, args: string[]) => {
1806
+ const [, sub, action, target] = args;
1807
+ if (sub === "marketplace") {
1808
+ const reg = existsSync(registryPath())
1809
+ ? JSON.parse(readFileSync(registryPath(), "utf8"))
1810
+ : {};
1811
+ if (action === "add" && target?.startsWith("owner/")) {
1812
+ const [repo, ref] = target.split("#");
1813
+ reg.mkt = githubEntry(repo, ref, installDir(true));
1814
+ } else if (action === "remove") {
1815
+ delete reg[target as string];
1816
+ }
1817
+ writeFileSync(registryPath(), JSON.stringify(reg));
1818
+ }
1819
+ return { stdout: "", exitCode: 0 };
1820
+ };
1821
+ const firstCalls: string[][] = [];
1822
+ const secondCalls: string[][] = [];
1823
+ await run(async (c, a) => (firstCalls.push(a), cliExec(c, a)), [ROW]);
1824
+ await run(async (c, a) => (secondCalls.push(a), cliExec(c, a)), [ROW]);
1825
+ expect(firstCalls.map((c) => c[2])).toEqual(["add", "update"]);
1826
+ expect(secondCalls).toEqual([["plugin", "marketplace", "update", "mkt"]]);
1827
+ });
1828
+ });
package/setup.ts CHANGED
@@ -382,30 +382,168 @@ export function findStalePluginSpecs(
382
382
  });
383
383
  }
384
384
 
385
+ /** A remote marketplace row from the agent config bundle. */
386
+ export interface RemoteMarketplace {
387
+ name: string;
388
+ source: string;
389
+ ref?: string | null;
390
+ }
391
+
392
+ /** Name of the built-in marketplace: its manifest name, else `shipwright`. */
393
+ function builtinMarketplaceName(repoRoot: string): string {
394
+ try {
395
+ const manifest = JSON.parse(
396
+ readFileSync(
397
+ join(repoRoot, ".claude-plugin", "marketplace.json"),
398
+ "utf8",
399
+ ),
400
+ );
401
+ if (typeof manifest?.name === "string") return manifest.name;
402
+ } catch {}
403
+ return "shipwright";
404
+ }
405
+
406
+ interface RegisteredMarketplace {
407
+ source?: { repo?: unknown; url?: unknown; path?: unknown; ref?: unknown };
408
+ installLocation?: unknown;
409
+ }
410
+
385
411
  /**
386
412
  * Reads the Claude CLI's known-marketplaces registry
387
- * (`known_marketplaces.json`, a sibling of `installed_plugins.json`) and
388
- * returns the install location it records for `name`. Defensive: returns
389
- * `undefined` for a missing or unparsable registry or an unknown name, and
413
+ * (`known_marketplaces.json`, a sibling of `installed_plugins.json`).
414
+ * Defensive: returns an empty registry for a missing or unparsable file, and
390
415
  * warns when the file exists but cannot be read.
391
416
  */
392
- function readRegisteredMarketplaceLocation(
417
+ function readMarketplaceRegistry(
393
418
  registryPath: string,
394
- name: string,
395
- ): string | undefined {
396
- if (!existsSync(registryPath)) return undefined;
419
+ ): Record<string, RegisteredMarketplace | null> {
420
+ if (!existsSync(registryPath)) return {};
397
421
  try {
398
- const registry = JSON.parse(readFileSync(registryPath, "utf8")) as Record<
399
- string,
400
- { installLocation?: unknown } | null
401
- >;
402
- const location = registry?.[name]?.installLocation;
403
- return typeof location === "string" ? location : undefined;
422
+ const registry = JSON.parse(readFileSync(registryPath, "utf8"));
423
+ return registry && typeof registry === "object" ? registry : {};
404
424
  } catch (err) {
405
425
  console.warn(
406
426
  `[agent] could not read ${registryPath} (nothing to heal): ${(err as Error).message}`,
407
427
  );
408
- return undefined;
428
+ return {};
429
+ }
430
+ }
431
+
432
+ /** The install location the registry records for `name`, if any. */
433
+ function readRegisteredMarketplaceLocation(
434
+ registryPath: string,
435
+ name: string,
436
+ ): string | undefined {
437
+ const location = readMarketplaceRegistry(registryPath)[name]?.installLocation;
438
+ return typeof location === "string" ? location : undefined;
439
+ }
440
+
441
+ /** Normalises a marketplace source (`owner/repo` or git URL) for comparison. */
442
+ function normalizeMarketplaceSource(source: string): string {
443
+ return source
444
+ .trim()
445
+ .replace(/^https:\/\/github\.com\//i, "")
446
+ .replace(/\/+$/, "")
447
+ .replace(/\.git$/i, "")
448
+ .toLowerCase();
449
+ }
450
+
451
+ /** Whether a registry entry was added from the same source and ref as `row`. */
452
+ function registeredMatchesRow(
453
+ entry: RegisteredMarketplace | null | undefined,
454
+ row: RemoteMarketplace,
455
+ ): boolean {
456
+ const src = entry?.source;
457
+ const registeredSource = [src?.repo, src?.url, src?.path].find(
458
+ (v): v is string => typeof v === "string",
459
+ );
460
+ if (registeredSource === undefined) return false;
461
+ const registeredRef = typeof src?.ref === "string" ? src.ref : null;
462
+ return (
463
+ normalizeMarketplaceSource(registeredSource) ===
464
+ normalizeMarketplaceSource(row.source) &&
465
+ registeredRef === (row.ref ?? null)
466
+ );
467
+ }
468
+
469
+ /** Runs `claude plugin marketplace <args>`; a non-zero exit warns, never throws. */
470
+ async function runMarketplaceCommand(
471
+ execFn: ExecFn,
472
+ cwd: string,
473
+ args: string[],
474
+ ): Promise<boolean> {
475
+ const result = await execFn("claude", ["plugin", "marketplace", ...args], {
476
+ cwd,
477
+ });
478
+ if (result.exitCode !== 0) {
479
+ console.warn(
480
+ `[agent] claude plugin marketplace ${args.join(" ")} exited ${result.exitCode}: ${result.stdout}`,
481
+ );
482
+ }
483
+ return result.exitCode === 0;
484
+ }
485
+
486
+ /**
487
+ * Reconciles one remote marketplace row (from the agent config bundle) to its
488
+ * desired state, then updates it:
489
+ * 1. a baked/built-in marketplace of the same name wins (warn, skip)
490
+ * 2. registered with another source/ref, or with an installLocation that has
491
+ * no marketplace.json -> remove first
492
+ * 3. not registered -> add `<source>[#ref]`
493
+ * 4. update
494
+ * Only the row's own name is ever touched. Failures warn and continue.
495
+ */
496
+ async function reconcileRemoteMarketplace(
497
+ row: RemoteMarketplace,
498
+ managedLocalNames: Set<string>,
499
+ registryPath: string,
500
+ execFn: ExecFn,
501
+ cwd: string,
502
+ ): Promise<void> {
503
+ if (managedLocalNames.has(row.name)) {
504
+ console.warn(
505
+ `[agent] remote marketplace ${row.name} ignored: a baked marketplace with the same name takes precedence`,
506
+ );
507
+ return;
508
+ }
509
+
510
+ const entry = readMarketplaceRegistry(registryPath)[row.name];
511
+ let registered = entry !== undefined && entry !== null;
512
+ if (registered) {
513
+ const location =
514
+ typeof entry?.installLocation === "string"
515
+ ? entry.installLocation
516
+ : undefined;
517
+ const dangling =
518
+ location === undefined ||
519
+ !existsSync(join(location, ".claude-plugin", "marketplace.json"));
520
+ if (dangling || !registeredMatchesRow(entry, row)) {
521
+ console.log(
522
+ `[agent] marketplace ${row.name} is registered with a different source/ref or no marketplace.json — removing before re-adding ${row.source}`,
523
+ );
524
+ await runMarketplaceCommand(execFn, cwd, ["remove", row.name]);
525
+ registered = false;
526
+ }
527
+ }
528
+
529
+ if (!registered) {
530
+ const spec = row.ref ? `${row.source}#${row.ref}` : row.source;
531
+ await runMarketplaceCommand(execFn, cwd, ["add", spec]);
532
+ }
533
+ await runMarketplaceCommand(execFn, cwd, ["update", row.name]);
534
+
535
+ // The CLI registers a marketplace under its manifest name, so a mismatch
536
+ // leaves the registry without an entry for the row's name.
537
+ const after = readMarketplaceRegistry(registryPath);
538
+ if (after[row.name] === undefined) {
539
+ const actual = Object.entries(after).find(([, e]) =>
540
+ registeredMatchesRow(e, row),
541
+ )?.[0];
542
+ if (actual !== undefined) {
543
+ console.warn(
544
+ `[agent] remote marketplace row ${row.name} cloned a manifest named ${actual} — plugin specs must use @${actual}`,
545
+ );
546
+ }
409
547
  }
410
548
  }
411
549
 
@@ -446,6 +584,7 @@ export async function installPlugins(
446
584
  extraMarketplaceDirs?: string[],
447
585
  extraMarketplacesRoot: string = BAKED_MARKETPLACES_ROOT,
448
586
  env: Record<string, string | undefined> = process.env,
587
+ remoteMarketplaces: RemoteMarketplace[] = [],
449
588
  ): Promise<void> {
450
589
  const shipwrightRepoRoot =
451
590
  shipwrightRepoRootOverride ?? resolveShipwrightRepoRoot(env);
@@ -477,16 +616,7 @@ export async function installPlugins(
477
616
  console.log(
478
617
  `[agent] marketplace ${name} is registered at ${registered} with no marketplace.json — removing before re-adding ${dir}`,
479
618
  );
480
- const remove = await execFn(
481
- "claude",
482
- ["plugin", "marketplace", "remove", name],
483
- { cwd },
484
- );
485
- if (remove.exitCode !== 0) {
486
- console.warn(
487
- `[agent] claude plugin marketplace remove ${name} exited ${remove.exitCode}: ${remove.stdout}`,
488
- );
489
- }
619
+ await runMarketplaceCommand(execFn, cwd, ["remove", name]);
490
620
  }
491
621
  const extraAdd = await execFn(
492
622
  "claude",
@@ -526,6 +656,21 @@ export async function installPlugins(
526
656
  }
527
657
  }
528
658
 
659
+ // Reconcile remote marketplaces (config bundle rows) after every local one
660
+ // is registered, before any plugin install.
661
+ const managedLocalNames = new Set(extraDirs.map((dir) => basename(dir)));
662
+ if (hasRepoMarketplace)
663
+ managedLocalNames.add(builtinMarketplaceName(shipwrightRepoRoot));
664
+ for (const row of remoteMarketplaces) {
665
+ await reconcileRemoteMarketplace(
666
+ row,
667
+ managedLocalNames,
668
+ registryPath,
669
+ execFn,
670
+ cwd,
671
+ );
672
+ }
673
+
529
674
  const allSpecs = agentPlugins.map((p) => `${p.plugin}@${p.marketplace}`);
530
675
 
531
676
  // Uninstall any plugin whose recorded installPath no longer exists. This