@app-vitals/agent-runtime 1.2.5 → 1.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-runtime",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
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.2.5",
44
- "@app-vitals/agent-lib": "1.2.5",
43
+ "@app-vitals/agent-admin": "1.2.7",
44
+ "@app-vitals/agent-lib": "1.2.7",
45
45
  "@octokit/auth-app": "^8.2.0",
46
46
  "@sentry/bun": "^10.63.0",
47
47
  "@slack/bolt": "^5.0.0",
@@ -1489,3 +1489,104 @@ describe("installPlugins — extra marketplace discovery", () => {
1489
1489
  expect(calls).toHaveLength(3);
1490
1490
  });
1491
1491
  });
1492
+
1493
+ describe("installPlugins stale shipwright marketplace healing", () => {
1494
+ type Exec = (
1495
+ cmd: string,
1496
+ args: string[],
1497
+ opts: { cwd: string },
1498
+ ) => Promise<{ stdout: string; exitCode: number }>;
1499
+ const record = () => {
1500
+ const calls: string[][] = [];
1501
+ const exec: Exec = async (_cmd, args) => {
1502
+ calls.push(args);
1503
+ return { stdout: "", exitCode: 0 };
1504
+ };
1505
+ return { calls, exec };
1506
+ };
1507
+ const noMarketplaceRoot = () => join(testHome, "no-marketplace-here");
1508
+ const bakedShipwright = () => {
1509
+ const dir = join(testHome, "baked", "shipwright");
1510
+ mkdirSync(join(dir, ".claude-plugin"), { recursive: true });
1511
+ writeFileSync(join(dir, ".claude-plugin", "marketplace.json"), "{}");
1512
+ return dir;
1513
+ };
1514
+ const writeRegistry = (content: string) => {
1515
+ mkdirSync(join(testHome, "plugins"), { recursive: true });
1516
+ writeFileSync(
1517
+ join(testHome, "plugins", "known_marketplaces.json"),
1518
+ content,
1519
+ );
1520
+ };
1521
+ const run = (exec: Exec, root: string, dirs: string[]) =>
1522
+ installPlugins(
1523
+ exec,
1524
+ testHome,
1525
+ [],
1526
+ root,
1527
+ join(testHome, "plugins", "installed_plugins.json"),
1528
+ dirs,
1529
+ );
1530
+
1531
+ it("skips the repo-root add when only a baked shipwright marketplace exists", async () => {
1532
+ const { calls, exec } = record();
1533
+ const baked = bakedShipwright();
1534
+ await run(exec, noMarketplaceRoot(), [baked]);
1535
+ expect(calls).toEqual([["plugin", "marketplace", "add", baked]]);
1536
+ });
1537
+
1538
+ it("removes a stale registration before adding the baked marketplace", async () => {
1539
+ const { calls, exec } = record();
1540
+ const baked = bakedShipwright();
1541
+ const stale = join(testHome, "stale-app");
1542
+ mkdirSync(stale, { recursive: true });
1543
+ writeRegistry(JSON.stringify({ shipwright: { installLocation: stale } }));
1544
+ await run(exec, noMarketplaceRoot(), [baked]);
1545
+ expect(calls).toEqual([
1546
+ ["plugin", "marketplace", "remove", "shipwright"],
1547
+ ["plugin", "marketplace", "add", baked],
1548
+ ]);
1549
+ });
1550
+
1551
+ it("does not remove when the registered location still has a marketplace", async () => {
1552
+ const { calls, exec } = record();
1553
+ const baked = bakedShipwright();
1554
+ writeRegistry(JSON.stringify({ shipwright: { installLocation: baked } }));
1555
+ await run(exec, noMarketplaceRoot(), [baked]);
1556
+ expect(calls).toEqual([["plugin", "marketplace", "add", baked]]);
1557
+ });
1558
+
1559
+ it("attempts no remove and warns when the registry is malformed", async () => {
1560
+ const { calls, exec } = record();
1561
+ const baked = bakedShipwright();
1562
+ writeRegistry("{not json");
1563
+ const warn = console.warn;
1564
+ const warnings: string[] = [];
1565
+ console.warn = (...a: unknown[]) => {
1566
+ warnings.push(a.join(" "));
1567
+ };
1568
+ try {
1569
+ await run(exec, noMarketplaceRoot(), [baked]);
1570
+ } finally {
1571
+ console.warn = warn;
1572
+ }
1573
+ expect(calls).toEqual([["plugin", "marketplace", "add", baked]]);
1574
+ expect(warnings.some((w) => w.includes("known_marketplaces.json"))).toBe(
1575
+ true,
1576
+ );
1577
+ });
1578
+
1579
+ it("attempts no remove when the registry is absent", async () => {
1580
+ const { calls, exec } = record();
1581
+ const baked = bakedShipwright();
1582
+ await run(exec, noMarketplaceRoot(), [baked]);
1583
+ expect(calls).toEqual([["plugin", "marketplace", "add", baked]]);
1584
+ });
1585
+
1586
+ it("keeps the coding-image sequence unchanged when the repo root is a marketplace", async () => {
1587
+ const { calls, exec } = record();
1588
+ const root = repoRoot();
1589
+ await run(exec, root, []);
1590
+ expect(calls).toEqual([["plugin", "marketplace", "add", root]]);
1591
+ });
1592
+ });
package/setup.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  unlinkSync,
21
21
  writeFileSync,
22
22
  } from "node:fs";
23
- import { join, resolve } from "node:path";
23
+ import { basename, dirname, join, resolve } from "node:path";
24
24
  import type { AgentPlugin } from "@app-vitals/agent-admin";
25
25
  import {
26
26
  resolveDefaultSpecialtyName,
@@ -382,6 +382,33 @@ export function findStalePluginSpecs(
382
382
  });
383
383
  }
384
384
 
385
+ /**
386
+ * 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
390
+ * warns when the file exists but cannot be read.
391
+ */
392
+ function readRegisteredMarketplaceLocation(
393
+ registryPath: string,
394
+ name: string,
395
+ ): string | undefined {
396
+ if (!existsSync(registryPath)) return undefined;
397
+ 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;
404
+ } catch (err) {
405
+ console.warn(
406
+ `[agent] could not read ${registryPath} (nothing to heal): ${(err as Error).message}`,
407
+ );
408
+ return undefined;
409
+ }
410
+ }
411
+
385
412
  /**
386
413
  * Installs the plugins in the agent's own AgentPlugin rows (seeded from the
387
414
  * specialty manifest at create time). No plugin is installed by default. All commands are idempotent (exit 0 when already
@@ -432,7 +459,35 @@ export async function installPlugins(
432
459
 
433
460
  // Register extra baked marketplaces BEFORE the built-in shipwright marketplace.
434
461
  // Idempotent — safe to call on every startup.
462
+ const registryPath = join(
463
+ dirname(pluginManifestPath),
464
+ "known_marketplaces.json",
465
+ );
435
466
  for (const dir of extraDirs) {
467
+ // Heal a stale registration: a persistent ~/.claude may still map this
468
+ // marketplace name to a directory that no longer holds a marketplace
469
+ // (e.g. `shipwright` -> /app on the specialist image). Remove it so the
470
+ // add below registers the baked path.
471
+ const name = basename(dir);
472
+ const registered = readRegisteredMarketplaceLocation(registryPath, name);
473
+ if (
474
+ registered !== undefined &&
475
+ !existsSync(join(registered, ".claude-plugin", "marketplace.json"))
476
+ ) {
477
+ console.log(
478
+ `[agent] marketplace ${name} is registered at ${registered} with no marketplace.json — removing before re-adding ${dir}`,
479
+ );
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
+ }
490
+ }
436
491
  const extraAdd = await execFn(
437
492
  "claude",
438
493
  ["plugin", "marketplace", "add", dir],
@@ -447,9 +502,18 @@ export async function installPlugins(
447
502
 
448
503
  // Register the local (built-in) shipwright marketplace so `claude plugin install`
449
504
  // can resolve plugins by name. Idempotent — safe to call on every startup.
505
+ const hasRepoMarketplace = existsSync(
506
+ join(shipwrightRepoRoot, ".claude-plugin", "marketplace.json"),
507
+ );
450
508
  if (
451
- existsSync(join(shipwrightRepoRoot, ".claude-plugin", "marketplace.json"))
509
+ !hasRepoMarketplace &&
510
+ extraDirs.some((dir) => basename(dir) === "shipwright")
452
511
  ) {
512
+ console.log(
513
+ `[agent] ${shipwrightRepoRoot} has no marketplace.json — using baked shipwright marketplace`,
514
+ );
515
+ }
516
+ if (hasRepoMarketplace) {
453
517
  const add = await execFn(
454
518
  "claude",
455
519
  ["plugin", "marketplace", "add", shipwrightRepoRoot],