@danypops/pi-packed 0.23.0 → 0.25.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.
@@ -74,11 +74,10 @@ export async function registerPackedVehicle(pi: ExtensionAPI): Promise<void> {
74
74
  await registerVehicleTools(pi, client, {
75
75
  permissions: ["packed:read"],
76
76
  principal: { id: "pi-packed" },
77
- // ownVehicleName must match daemon.ts's own PACKED_VEHICLE_NAME (the shared Vehicle Handle
78
- // Directory entry Packed's own daemon registers under via daemonOptions()'s vehicleName --
79
- // see service/src/daemon/daemon.ts). activateForeignOperation is auto-supplied by
80
- // registerVehicleTools. See enable-vehicle-shell-broker-mode-in-pi-packed.
81
- shell: { coreOperations: CORE_OPERATIONS, broker: { ownVehicleName: "pi-packed" } },
77
+ // tools_list/tools_man are a neutral, process-wide singleton now (vehicle-client-pi's own
78
+ // ensureVehicleShellHandle) -- no ownVehicleName/broker option needed here anymore; every
79
+ // vehicle in the process (including packed's own) is discovered and namespaced uniformly.
80
+ shell: { coreOperations: CORE_OPERATIONS },
82
81
  });
83
82
  } catch {
84
83
  // Daemon state is stale/unreachable -- degrade silently, matching
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,11 +27,11 @@
27
27
  "typecheck": "bunx tsc --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "@danypops/armada": "^0.4.3",
30
+ "@danypops/armada": "^0.4.6",
31
31
  "@danypops/packed": "^0.7.0",
32
32
  "@danypops/pi-extension-harness": "^0.7.0",
33
33
  "@danypops/vehicle-client": "^0.8.1",
34
- "@danypops/vehicle-client-pi": "^0.39.0",
34
+ "@danypops/vehicle-client-pi": "^0.40.1",
35
35
  "@danypops/vehicle-core": "^0.15.0",
36
36
  "@danypops/vehicle-server": "^0.22.0",
37
37
  "jiti": "^2.7.0",
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { readdirSync, readFileSync } from "node:fs";
29
29
  import { join } from "node:path";
30
- import { createVehicleRegistrar, type VehicleRegistrar } from "@danypops/armada";
30
+ import { createVehicleRegistrar, type VehicleRegistrar, type VehicleRegistrationOutcome, type VehicleSpec } from "@danypops/armada";
31
31
  import { resolveDaemonPaths } from "@danypops/vehicle-server/paths";
32
32
  import {
33
33
  isVehicleServiceRegistered,
@@ -408,6 +408,15 @@ export interface ReconcileAllResult {
408
408
  reconciled: Array<{ packageName: string; vehicleName: string; installed: boolean; reason?: string }>;
409
409
  skipped: number;
410
410
  failed: Array<{ packageName: string; reason: string }>;
411
+ /**
412
+ * Every Vehicle unregistered because this same sweep could no longer
413
+ * discover it by any path (neither a configured extension's own
414
+ * dependency walk nor listUnconfiguredDaemonDependencies' root-pinned
415
+ * scan) -- see pruneStaleVehicles' own doc comment for why this is safe
416
+ * to do unconditionally rather than requiring a separate opt-in call.
417
+ */
418
+ pruned: Array<{ vehicleName: string; executable: string }>;
419
+ pruneFailed: Array<{ vehicleName: string; reason: string }>;
411
420
  }
412
421
 
413
422
  /** Matches readPackageDeclarations' own bound -- a reconcile-all sweep never processes an unbounded package list. */
@@ -438,7 +447,7 @@ const SELF_REGISTRATION_REASON = "Packed cannot replace its own Armada service f
438
447
  export async function reconcileAllDaemonServices(
439
448
  piHome: string,
440
449
  projectRoot: string | undefined,
441
- installer: Pick<DaemonServiceInstaller, "install">,
450
+ installer: Pick<DaemonServiceInstaller, "install" | "listRegisteredVehicles" | "unregisterVehicleByName">,
442
451
  ): Promise<ReconcileAllResult> {
443
452
  const packages = readInstalledPackagesAcrossScopes(piHome, projectRoot).slice(0, MAX_RECONCILE_PACKAGES);
444
453
  const reconciled: ReconcileAllResult["reconciled"] = [];
@@ -470,7 +479,62 @@ export async function reconcileAllDaemonServices(
470
479
  ...(resolved.result.installed ? {} : { reason: resolved.result.reason }),
471
480
  });
472
481
  }
473
- return { reconciled, skipped, failed };
482
+ // listUnconfiguredDaemonDependencies deliberately walks the FULL top-level
483
+ // node_modules sweep, not just readInstalledPackagesAcrossScopes' own
484
+ // pi:-configured list above -- a root-pinned dependency (e.g. lector,
485
+ // pinned directly in piHome/npm/package.json ahead of whatever version its
486
+ // own pi-lector wrapper's tree would resolve) is a real, currently-running
487
+ // Vehicle that the configured-packages loop above never reaches at all.
488
+ // Folding it in here is what makes the discovered set below actually
489
+ // complete -- pruning against anything narrower would delete a Vehicle
490
+ // that's still genuinely there, just reachable by a different path.
491
+ for (const dep of listUnconfiguredDaemonDependencies(piHome)) seen.add(dep.vehicleName);
492
+ const { pruned, pruneFailed } = await pruneStaleVehicles(piHome, seen, installer);
493
+ return { reconciled, skipped, failed, pruned, pruneFailed };
494
+ }
495
+
496
+ /**
497
+ * Unregisters every Vehicle Armada currently declares that `discovered`
498
+ * (this same sweep's own complete union of configured-extension and
499
+ * root-pinned resolution) no longer produces at all -- the other half of
500
+ * what makes Armada authoritative for every Vehicle Packed knows about.
501
+ * Without this, a Vehicle whose packed.daemonService.name changed (or that
502
+ * a stale nested dependency copy once resolved under a different name --
503
+ * see this session's own live incident, a duplicate "web-spider-daemon"
504
+ * losing every restart's single-instance-lock race against the correctly-
505
+ * named "web-spider" Vehicle) just sits in the manifest forever: nothing
506
+ * ever re-discovers it to overwrite it, and nothing ever notices it's gone
507
+ * stale either.
508
+ *
509
+ * Scoped to only ever touch a Vehicle whose own `executable` lives under
510
+ * piHome's own npm/node_modules -- i.e. one only Packed itself could
511
+ * plausibly have registered in the first place. A Vehicle registered by an
512
+ * entirely different mechanism (a hand-rolled `armada upsert` pointing
513
+ * somewhere else on disk) is never a candidate, no matter how the
514
+ * discovered set above turns out -- this function has no way to know that
515
+ * caller's own intent, so it stays out of its way entirely. Packed's own
516
+ * Vehicle (PACKED_VEHICLE_NAME) is excluded for the same reason
517
+ * install()/remove()/restart() already refuse to touch it: reconcile runs
518
+ * from inside Packed's own process.
519
+ */
520
+ async function pruneStaleVehicles(
521
+ piHome: string,
522
+ discovered: ReadonlySet<string>,
523
+ installer: Pick<DaemonServiceInstaller, "listRegisteredVehicles" | "unregisterVehicleByName">,
524
+ ): Promise<Pick<ReconcileAllResult, "pruned" | "pruneFailed">> {
525
+ const managedRoot = join(piHome, "npm", "node_modules") + "/";
526
+ const pruned: ReconcileAllResult["pruned"] = [];
527
+ const pruneFailed: ReconcileAllResult["pruneFailed"] = [];
528
+ const registered = await installer.listRegisteredVehicles();
529
+ for (const vehicle of registered) {
530
+ if (vehicle.name === PACKED_VEHICLE_NAME) continue;
531
+ if (discovered.has(vehicle.name)) continue;
532
+ if (!vehicle.executable.startsWith(managedRoot)) continue;
533
+ const outcome = await installer.unregisterVehicleByName(vehicle.name);
534
+ if (outcome.ok) pruned.push({ vehicleName: vehicle.name, executable: vehicle.executable });
535
+ else pruneFailed.push({ vehicleName: vehicle.name, reason: outcome.diagnostics.map((d) => d.message).join("; ") || "unregister failed" });
536
+ }
537
+ return { pruned, pruneFailed };
474
538
  }
475
539
 
476
540
  export interface DaemonServiceInstaller {
@@ -486,6 +550,15 @@ export interface DaemonServiceInstaller {
486
550
  piHome: string,
487
551
  source: string,
488
552
  ): Promise<{ ok: true; restarted: boolean; reason?: string; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }>;
553
+ /** Every Vehicle Armada currently declares -- see pruneStaleVehicles' own doc comment for why this exists. */
554
+ listRegisteredVehicles(): Promise<readonly VehicleSpec[]>;
555
+ /**
556
+ * Unregisters by Armada vehicle NAME directly, distinct from remove()'s
557
+ * own by-npm-source resolution -- pruning has no installed package left to
558
+ * resolve a source from; the whole point is the vehicle survived past
559
+ * whatever used to produce it.
560
+ */
561
+ unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome>;
489
562
  }
490
563
 
491
564
  /**
@@ -498,6 +571,14 @@ export interface DaemonServiceInstaller {
498
571
  export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
499
572
  constructor(private readonly registrar: VehicleRegistrar = createVehicleRegistrar()) {}
500
573
 
574
+ async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
575
+ return this.registrar.listRegistered();
576
+ }
577
+
578
+ async unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome> {
579
+ return this.registrar.unregister(name);
580
+ }
581
+
501
582
  async install(
502
583
  piHome: string,
503
584
  source: string,
@@ -123,6 +123,8 @@ class FakeDaemonServiceInstaller {
123
123
  reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
124
124
  skipped: 0,
125
125
  failed: [],
126
+ pruned: [],
127
+ pruneFailed: [],
126
128
  };
127
129
  }
128
130
  }
@@ -605,6 +607,8 @@ describe("CLI", () => {
605
607
  reconciled: [{ packageName: "@danypops/probe", vehicleName: "probe", installed: true }],
606
608
  skipped: 0,
607
609
  failed: [],
610
+ pruned: [],
611
+ pruneFailed: [],
608
612
  });
609
613
  });
610
614
 
@@ -1007,7 +1011,15 @@ describe("CLI", () => {
1007
1011
  },
1008
1012
  async reconcileServices(approved, projectRoot) {
1009
1013
  calls.push(`reconcileServices:${approved}:${projectRoot}`);
1010
- return { ok: true, output: "reconciled 0 Vehicle(s), skipped 0 non-daemon package(s)", reconciled: [], skipped: 0, failed: [] };
1014
+ return {
1015
+ ok: true,
1016
+ output: "reconciled 0 Vehicle(s), skipped 0 non-daemon package(s)",
1017
+ reconciled: [],
1018
+ skipped: 0,
1019
+ failed: [],
1020
+ pruned: [],
1021
+ pruneFailed: [],
1022
+ };
1011
1023
  },
1012
1024
  async update(source) {
1013
1025
  return { output: source, reloadRequired: false, alreadyUpToDate: true, pinned: false };
@@ -1303,6 +1315,12 @@ describe("daemon client", () => {
1303
1315
  spec: { name: "pi-lsp", version: "1.0.0", binPath: "/opt/pi-lsp/cli.js", handlePath: "/tmp/pi-lsp.handle.json" },
1304
1316
  };
1305
1317
  },
1318
+ async listRegisteredVehicles() {
1319
+ return [];
1320
+ },
1321
+ async unregisterVehicleByName() {
1322
+ return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
1323
+ },
1306
1324
  },
1307
1325
  token: daemonToken,
1308
1326
  stateDir: daemonDir,
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import type { VehicleRegistrationOutcome, VehicleSpec } from "@danypops/armada";
5
6
  import { createArmadaTestHarness } from "@danypops/armada/testing";
6
7
  import type { MaintenanceTask } from "@danypops/vehicle-server/daemon";
7
8
  import type { ServiceInstallResult, ServiceSpec } from "@danypops/vehicle-server/service";
@@ -121,6 +122,12 @@ class RecordingDaemonServiceInstaller {
121
122
  > {
122
123
  return { ok: false, reason: "not a daemon", notADaemon: true };
123
124
  }
125
+ async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
126
+ return [];
127
+ }
128
+ async unregisterVehicleByName(): Promise<VehicleRegistrationOutcome> {
129
+ return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
130
+ }
124
131
  }
125
132
 
126
133
  describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle drift, not just per-package on demand)", () => {
@@ -308,4 +315,171 @@ describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle dr
308
315
  expect(snapshot?.updates.map((u) => u.name)).toContain("@danypops/probe-daemon");
309
316
  });
310
317
  });
318
+
319
+ describe("reconcileAllDaemonServices pruning -- unregisters a Vehicle no longer discoverable by any path (the-armada-registrar-jittor-web-spider-daemon-collision)", () => {
320
+ function stalePiHomeVehicle(piHome: string, name: string): VehicleSpec {
321
+ return {
322
+ name: name as VehicleSpec["name"],
323
+ version: "0.1.0",
324
+ executable: join(piHome, "npm", "node_modules", "@danypops", "old-package", "cli.ts"),
325
+ arguments: ["serve"],
326
+ handlePath: join(piHome, "stale-handle.json"),
327
+ restart: { policy: "never" },
328
+ readiness: { timeoutMs: 500, pollIntervalMs: 50 },
329
+ };
330
+ }
331
+
332
+ it("prunes a Vehicle whose executable lives under piHome's own node_modules once nothing discovers it anymore", async () => {
333
+ const piHome = fakePiHome([]); // deliberately empty -- old-package no longer configured or on disk
334
+ const harness = await createArmadaTestHarness();
335
+ try {
336
+ await harness.registrar.register(stalePiHomeVehicle(piHome, "old-package"));
337
+ expect(await harness.registrar.isRegistered("old-package")).toBe(true);
338
+
339
+ const installer = new RealDaemonServiceInstaller(harness.registrar);
340
+ const result = await reconcileAllDaemonServices(piHome, undefined, installer);
341
+
342
+ expect(result.pruned).toEqual([{ vehicleName: "old-package", executable: stalePiHomeVehicle(piHome, "old-package").executable }]);
343
+ expect(result.pruneFailed).toEqual([]);
344
+ expect(await harness.registrar.isRegistered("old-package")).toBe(false);
345
+ } finally {
346
+ await harness.dispose();
347
+ }
348
+ });
349
+
350
+ it("never prunes a Vehicle registered outside piHome's own node_modules, regardless of discovery", async () => {
351
+ const piHome = fakePiHome([]);
352
+ const harness = await createArmadaTestHarness();
353
+ try {
354
+ await harness.registrar.register({
355
+ name: "externally-managed" as VehicleSpec["name"],
356
+ version: "1.0.0",
357
+ executable: "/opt/custom/cli.js",
358
+ arguments: ["serve"],
359
+ handlePath: join(piHome, "external-handle.json"),
360
+ restart: { policy: "never" },
361
+ readiness: { timeoutMs: 500, pollIntervalMs: 50 },
362
+ });
363
+
364
+ const installer = new RealDaemonServiceInstaller(harness.registrar);
365
+ const result = await reconcileAllDaemonServices(piHome, undefined, installer);
366
+
367
+ expect(result.pruned).toEqual([]);
368
+ expect(await harness.registrar.isRegistered("externally-managed")).toBe(true);
369
+ } finally {
370
+ await harness.dispose();
371
+ }
372
+ });
373
+
374
+ it("never prunes a Vehicle still discoverable through the configured-extension sweep -- only updates it", async () => {
375
+ const piHome = fakePiHome(["npm:@danypops/probe"]);
376
+ installMockDaemon(piHome, "@danypops/probe");
377
+ const harness = await createArmadaTestHarness();
378
+ try {
379
+ const installer = new RealDaemonServiceInstaller(harness.registrar);
380
+ const result = await reconcileAllDaemonServices(piHome, undefined, installer);
381
+
382
+ expect(result.pruned).toEqual([]);
383
+ expect(await harness.registrar.isRegistered("probe")).toBe(true);
384
+ } finally {
385
+ await harness.dispose();
386
+ }
387
+ });
388
+
389
+ it("never prunes a Vehicle still discoverable only through the unconfigured-daemon-dependency sweep (e.g. a root-pinned package like lector)", async () => {
390
+ const piHome = fakePiHome([]); // deliberately NOT settings.json-configured
391
+ installUnconfiguredDaemonDependency(piHome, "@danypops/probe-daemon", "1.0.0");
392
+ const harness = await createArmadaTestHarness();
393
+ try {
394
+ await harness.registrar.register({
395
+ name: "probe-daemon" as VehicleSpec["name"],
396
+ version: "1.0.0",
397
+ executable: join(piHome, "npm", "node_modules", "@danypops", "probe-daemon", "cli.ts"),
398
+ arguments: ["serve"],
399
+ handlePath: join(piHome, "probe-daemon-handle.json"),
400
+ restart: { policy: "never" },
401
+ readiness: { timeoutMs: 500, pollIntervalMs: 50 },
402
+ });
403
+
404
+ const installer = new RealDaemonServiceInstaller(harness.registrar);
405
+ const result = await reconcileAllDaemonServices(piHome, undefined, installer);
406
+
407
+ expect(result.pruned).toEqual([]);
408
+ expect(await harness.registrar.isRegistered("probe-daemon")).toBe(true);
409
+ } finally {
410
+ await harness.dispose();
411
+ }
412
+ });
413
+
414
+ it("never prunes pi-packed's own Vehicle, even though reconcileAllDaemonServices always skips scanning its own package", async () => {
415
+ const piHome = fakePiHome([]);
416
+ const harness = await createArmadaTestHarness();
417
+ try {
418
+ await harness.registrar.register({
419
+ name: "pi-packed" as VehicleSpec["name"],
420
+ version: "1.0.0",
421
+ executable: join(piHome, "npm", "node_modules", "@danypops", "pi-packed", "service", "src", "cli", "cli.ts"),
422
+ arguments: ["serve"],
423
+ handlePath: join(piHome, "pi-packed-handle.json"),
424
+ restart: { policy: "never" },
425
+ readiness: { timeoutMs: 500, pollIntervalMs: 50 },
426
+ });
427
+
428
+ const installer = new RealDaemonServiceInstaller(harness.registrar);
429
+ const result = await reconcileAllDaemonServices(piHome, undefined, installer);
430
+
431
+ expect(result.pruned).toEqual([]);
432
+ expect(await harness.registrar.isRegistered("pi-packed")).toBe(true);
433
+ } finally {
434
+ await harness.dispose();
435
+ }
436
+ });
437
+
438
+ it("reproduces the real web-spider/web-spider-daemon collision: the orphaned duplicate is pruned, the correctly-discovered one survives", async () => {
439
+ const piHome = fakePiHome(["npm:@danypops/pi-web-spider"]);
440
+ // pi-web-spider's own dependency-walk discovers @danypops/web-spider-daemon,
441
+ // whose packed.daemonService.name overrides the vehicle name to "web-spider".
442
+ const piWebSpiderDir = join(piHome, "npm", "node_modules", "@danypops", "pi-web-spider");
443
+ mkdirSync(piWebSpiderDir, { recursive: true });
444
+ writeFileSync(
445
+ join(piWebSpiderDir, "package.json"),
446
+ JSON.stringify({ name: "@danypops/pi-web-spider", version: "1.0.0", dependencies: { "@danypops/web-spider-daemon": "^1.0.0" } }),
447
+ );
448
+ const daemonDir = join(piHome, "npm", "node_modules", "@danypops", "web-spider-daemon");
449
+ mkdirSync(daemonDir, { recursive: true });
450
+ writeFileSync(
451
+ join(daemonDir, "package.json"),
452
+ JSON.stringify({
453
+ name: "@danypops/web-spider-daemon",
454
+ version: "1.0.0",
455
+ packed: { daemonService: { name: "web-spider", binPath: "cli.ts", args: ["serve"] } },
456
+ }),
457
+ );
458
+ writeFileSync(join(daemonDir, "cli.ts"), "// Mock Vehicle entry point.\n");
459
+
460
+ const harness = await createArmadaTestHarness();
461
+ try {
462
+ // A leftover registration from before the packed.daemonService.name override existed --
463
+ // the exact orphan a prior Packed version would have produced and never cleaned up.
464
+ await harness.registrar.register({
465
+ name: "web-spider-daemon" as VehicleSpec["name"],
466
+ version: "0.9.0",
467
+ executable: join(daemonDir, "cli.ts"),
468
+ arguments: ["serve"],
469
+ handlePath: join(piHome, "web-spider-daemon-handle.json"),
470
+ restart: { policy: "never" },
471
+ readiness: { timeoutMs: 500, pollIntervalMs: 50 },
472
+ });
473
+
474
+ const installer = new RealDaemonServiceInstaller(harness.registrar);
475
+ const result = await reconcileAllDaemonServices(piHome, undefined, installer);
476
+
477
+ expect(result.pruned).toEqual([{ vehicleName: "web-spider-daemon", executable: join(daemonDir, "cli.ts") }]);
478
+ expect(await harness.registrar.isRegistered("web-spider-daemon")).toBe(false);
479
+ expect(await harness.registrar.isRegistered("web-spider")).toBe(true);
480
+ } finally {
481
+ await harness.dispose();
482
+ }
483
+ });
484
+ });
311
485
  });
@@ -500,6 +500,7 @@ describe("RealDaemonServiceInstaller -- Armada registration through the in-proce
500
500
  register: async () => ({ ok: false, diagnostics: [{ code: "X", severity: "error", path: "/", message: "native failure" }] }),
501
501
  unregister: async () => ({ ok: false, diagnostics: [] }),
502
502
  isRegistered: async () => false,
503
+ listRegistered: async () => [],
503
504
  };
504
505
  const installer = new RealDaemonServiceInstaller(registrar);
505
506
 
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import type { VehicleRegistrationOutcome, VehicleSpec } from "@danypops/armada";
5
6
  import type { ServiceSpec } from "@danypops/vehicle-server/service";
6
7
  import type { DaemonServiceInstaller } from "../src/daemon/daemon-service.ts";
7
8
  import { createApp, type Deps } from "../src/daemon/service.ts";
@@ -123,6 +124,16 @@ class FakeDaemonServiceInstaller implements DaemonServiceInstaller {
123
124
  return { ok: false, reason: this.restartResolveFailure, ...(this.restartNotADaemon ? { notADaemon: true } : {}) };
124
125
  return { ok: true, restarted: this.restarted, reason: this.restartReason, spec: this.spec };
125
126
  }
127
+ registeredVehicles: VehicleSpec[] = [];
128
+ unregisterByNameCalls: string[] = [];
129
+ async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
130
+ return this.registeredVehicles;
131
+ }
132
+ async unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome> {
133
+ this.unregisterByNameCalls.push(name);
134
+ this.registeredVehicles = this.registeredVehicles.filter((vehicle) => vehicle.name !== name);
135
+ return { ok: true, manifestHash: "hash" as never, applied: [], diagnostics: [] };
136
+ }
126
137
  }
127
138
 
128
139
  const roots: string[] = [];