@legioncodeinc/hive 0.2.0 → 0.3.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.
Files changed (108) hide show
  1. package/README.md +2 -2
  2. package/dist/daemon/dashboard/app.js +26 -25
  3. package/dist/daemon/dashboard/host.d.ts +7 -0
  4. package/dist/daemon/dashboard/host.js +16 -0
  5. package/dist/daemon/dashboard/host.js.map +1 -1
  6. package/dist/daemon/dashboard/web-assets.d.ts +2 -0
  7. package/dist/daemon/dashboard/web-assets.js +19 -0
  8. package/dist/daemon/dashboard/web-assets.js.map +1 -1
  9. package/dist/daemon/gate.d.ts +2 -2
  10. package/dist/daemon/gate.js +15 -4
  11. package/dist/daemon/gate.js.map +1 -1
  12. package/dist/daemon/installer/bin-resolver.d.ts +34 -0
  13. package/dist/daemon/installer/bin-resolver.js +86 -0
  14. package/dist/daemon/installer/bin-resolver.js.map +1 -0
  15. package/dist/daemon/installer/config.d.ts +63 -0
  16. package/dist/daemon/installer/config.js +74 -0
  17. package/dist/daemon/installer/config.js.map +1 -0
  18. package/dist/daemon/installer/detection.d.ts +20 -0
  19. package/dist/daemon/installer/detection.js +73 -0
  20. package/dist/daemon/installer/detection.js.map +1 -0
  21. package/dist/daemon/installer/funnel-telemetry.d.ts +54 -0
  22. package/dist/daemon/installer/funnel-telemetry.js +134 -0
  23. package/dist/daemon/installer/funnel-telemetry.js.map +1 -0
  24. package/dist/daemon/installer/index.d.ts +12 -0
  25. package/dist/daemon/installer/index.js +10 -0
  26. package/dist/daemon/installer/index.js.map +1 -0
  27. package/dist/daemon/installer/install-state.d.ts +50 -0
  28. package/dist/daemon/installer/install-state.js +142 -0
  29. package/dist/daemon/installer/install-state.js.map +1 -0
  30. package/dist/daemon/installer/manifest-snapshot.json +25 -0
  31. package/dist/daemon/installer/manifest.d.ts +47 -0
  32. package/dist/daemon/installer/manifest.js +103 -0
  33. package/dist/daemon/installer/manifest.js.map +1 -0
  34. package/dist/daemon/installer/products.d.ts +33 -0
  35. package/dist/daemon/installer/products.js +42 -0
  36. package/dist/daemon/installer/products.js.map +1 -0
  37. package/dist/daemon/installer/routes.d.ts +43 -0
  38. package/dist/daemon/installer/routes.js +201 -0
  39. package/dist/daemon/installer/routes.js.map +1 -0
  40. package/dist/daemon/installer/security.d.ts +33 -0
  41. package/dist/daemon/installer/security.js +80 -0
  42. package/dist/daemon/installer/security.js.map +1 -0
  43. package/dist/daemon/installer/spawn.d.ts +48 -0
  44. package/dist/daemon/installer/spawn.js +51 -0
  45. package/dist/daemon/installer/spawn.js.map +1 -0
  46. package/dist/daemon/installer/token.d.ts +23 -0
  47. package/dist/daemon/installer/token.js +56 -0
  48. package/dist/daemon/installer/token.js.map +1 -0
  49. package/dist/daemon/server.d.ts +6 -0
  50. package/dist/daemon/server.js +7 -0
  51. package/dist/daemon/server.js.map +1 -1
  52. package/dist/dashboard/web/app.js +42 -20
  53. package/dist/dashboard/web/app.js.map +1 -1
  54. package/dist/dashboard/web/boot-route.d.ts +11 -7
  55. package/dist/dashboard/web/boot-route.js +12 -6
  56. package/dist/dashboard/web/boot-route.js.map +1 -1
  57. package/dist/dashboard/web/main.js +2 -1
  58. package/dist/dashboard/web/main.js.map +1 -1
  59. package/dist/dashboard/web/onboarding/advanced-picker.d.ts +16 -0
  60. package/dist/dashboard/web/onboarding/advanced-picker.js +59 -0
  61. package/dist/dashboard/web/onboarding/advanced-picker.js.map +1 -0
  62. package/dist/dashboard/web/onboarding/contracts.d.ts +188 -0
  63. package/dist/dashboard/web/onboarding/contracts.js +161 -0
  64. package/dist/dashboard/web/onboarding/contracts.js.map +1 -0
  65. package/dist/dashboard/web/onboarding/health-view.d.ts +17 -0
  66. package/dist/dashboard/web/onboarding/health-view.js +79 -0
  67. package/dist/dashboard/web/onboarding/health-view.js.map +1 -0
  68. package/dist/dashboard/web/onboarding/install-card.d.ts +27 -0
  69. package/dist/dashboard/web/onboarding/install-card.js +170 -0
  70. package/dist/dashboard/web/onboarding/install-card.js.map +1 -0
  71. package/dist/dashboard/web/onboarding/login-step.d.ts +29 -0
  72. package/dist/dashboard/web/onboarding/login-step.js +104 -0
  73. package/dist/dashboard/web/onboarding/login-step.js.map +1 -0
  74. package/dist/dashboard/web/onboarding/onboarding-client.d.ts +52 -0
  75. package/dist/dashboard/web/onboarding/onboarding-client.js +133 -0
  76. package/dist/dashboard/web/onboarding/onboarding-client.js.map +1 -0
  77. package/dist/dashboard/web/onboarding/onboarding-hero.d.ts +24 -0
  78. package/dist/dashboard/web/onboarding/onboarding-hero.js +70 -0
  79. package/dist/dashboard/web/onboarding/onboarding-hero.js.map +1 -0
  80. package/dist/dashboard/web/onboarding/onboarding-screen.d.ts +43 -0
  81. package/dist/dashboard/web/onboarding/onboarding-screen.js +161 -0
  82. package/dist/dashboard/web/onboarding/onboarding-screen.js.map +1 -0
  83. package/dist/dashboard/web/onboarding/onboarding-selection-store.d.ts +20 -0
  84. package/dist/dashboard/web/onboarding/onboarding-selection-store.js +76 -0
  85. package/dist/dashboard/web/onboarding/onboarding-selection-store.js.map +1 -0
  86. package/dist/dashboard/web/onboarding/product-copy.d.ts +40 -0
  87. package/dist/dashboard/web/onboarding/product-copy.js +70 -0
  88. package/dist/dashboard/web/onboarding/product-copy.js.map +1 -0
  89. package/dist/dashboard/web/onboarding/use-install-dwell.d.ts +22 -0
  90. package/dist/dashboard/web/onboarding/use-install-dwell.js +37 -0
  91. package/dist/dashboard/web/onboarding/use-install-dwell.js.map +1 -0
  92. package/dist/dashboard/web/onboarding/use-onboarding-token.d.ts +9 -0
  93. package/dist/dashboard/web/onboarding/use-onboarding-token.js +34 -0
  94. package/dist/dashboard/web/onboarding/use-onboarding-token.js.map +1 -0
  95. package/dist/dashboard/web/route-daemon-owner.d.ts +7 -0
  96. package/dist/dashboard/web/route-daemon-owner.js +15 -0
  97. package/dist/dashboard/web/route-daemon-owner.js.map +1 -0
  98. package/dist/dashboard/web/wire.d.ts +1 -1
  99. package/dist/shared/onboarding-types.d.ts +79 -0
  100. package/dist/shared/onboarding-types.js +12 -0
  101. package/dist/shared/onboarding-types.js.map +1 -0
  102. package/dist/telemetry/emit.d.ts +30 -3
  103. package/dist/telemetry/emit.js +25 -2
  104. package/dist/telemetry/emit.js.map +1 -1
  105. package/dist/telemetry/onboarding-session-ledger.d.ts +31 -0
  106. package/dist/telemetry/onboarding-session-ledger.js +78 -0
  107. package/dist/telemetry/onboarding-session-ledger.js.map +1 -0
  108. package/package.json +1 -1
@@ -0,0 +1,134 @@
1
+ /**
2
+ * PRD-009c: daemon-side onboarding funnel emission.
3
+ *
4
+ * Every funnel byte leaves through {@link emitTelemetry} in `src/telemetry/emit.ts`. UI-originated
5
+ * milestones arrive via the token-gated event route; install and auth milestones are observed from
6
+ * daemon state transitions. Session-scoped dedupe lives in `onboarding-session-ledger.ts`.
7
+ */
8
+ import { z } from "zod";
9
+ import { emitTelemetry, HIVE_STATE_DIR } from "../../telemetry/emit.js";
10
+ import { isSessionEventReported, loadOnboardingLedger, markSessionEventReported, sessionKeyFromToken } from "../../telemetry/onboarding-session-ledger.js";
11
+ /** UI-reported funnel events validated at the event route (tm-AC-1). */
12
+ export const UI_FUNNEL_EVENTS = [
13
+ "onboarding_started",
14
+ "mode_selected",
15
+ "login_shown",
16
+ "dashboard_reached"
17
+ ];
18
+ const ModeSelectedBodySchema = z.object({
19
+ event: z.literal("mode_selected"),
20
+ properties: z.object({ mode: z.enum(["standard", "advanced"]) })
21
+ });
22
+ const SimpleUiEventBodySchema = z.object({
23
+ event: z.enum(["onboarding_started", "login_shown", "dashboard_reached"]),
24
+ properties: z.record(z.string(), z.string()).optional()
25
+ });
26
+ /** Closed zod schema for `POST /api/onboarding/event` bodies. */
27
+ export const OnboardingEventBodySchema = z.union([ModeSelectedBodySchema, SimpleUiEventBodySchema]);
28
+ const SESSION_ONCE_SET = new Set([
29
+ "onboarding_started",
30
+ "health_check_passed",
31
+ "login_completed",
32
+ "dashboard_reached"
33
+ ]);
34
+ const INSTALL_STAGES = [
35
+ "resolving",
36
+ "downloading",
37
+ "linking",
38
+ "registering_service"
39
+ ];
40
+ function failureStageFromInstallStage(stage) {
41
+ if (stage === "completed" || stage === "failed")
42
+ return null;
43
+ return INSTALL_STAGES.includes(stage) ? stage : null;
44
+ }
45
+ function readSessionKey(config) {
46
+ const raw = config.readTextFile(config.tokenPath);
47
+ if (raw === null)
48
+ return null;
49
+ const trimmed = raw.trim();
50
+ if (trimmed.length === 0)
51
+ return null;
52
+ return sessionKeyFromToken(trimmed);
53
+ }
54
+ function fireAndForget(run) {
55
+ void run().catch(() => {
56
+ // Fail-soft: telemetry never affects install behavior (tm-AC-3).
57
+ });
58
+ }
59
+ export function createFunnelTelemetry(deps) {
60
+ const stateDir = deps.stateDir ?? deps.emitDeps?.stateDir ?? HIVE_STATE_DIR;
61
+ const emitDeps = deps.emitDeps ?? {};
62
+ const clock = deps.clock ?? (() => new Date().toISOString());
63
+ let sawAuthenticated = false;
64
+ const emit = (event, extras = {}, dedupeSession = false) => {
65
+ fireAndForget(async () => {
66
+ const sessionKey = readSessionKey(deps.config);
67
+ if (dedupeSession && sessionKey !== null) {
68
+ if (!SESSION_ONCE_SET.has(event))
69
+ return;
70
+ const ledger = loadOnboardingLedger(stateDir);
71
+ if (isSessionEventReported(ledger, sessionKey, event))
72
+ return;
73
+ }
74
+ const outcome = await emitTelemetry(event, {}, emitDeps, extras);
75
+ if (outcome.sent &&
76
+ dedupeSession &&
77
+ sessionKey !== null &&
78
+ SESSION_ONCE_SET.has(event)) {
79
+ try {
80
+ markSessionEventReported(stateDir, sessionKey, event, clock);
81
+ }
82
+ catch {
83
+ // Persist hiccup after a successful send is non-fatal.
84
+ }
85
+ }
86
+ });
87
+ };
88
+ return {
89
+ recordUiEvent(body) {
90
+ switch (body.event) {
91
+ case "mode_selected":
92
+ emit("mode_selected", { mode: body.properties.mode });
93
+ break;
94
+ case "onboarding_started":
95
+ emit("onboarding_started", {}, true);
96
+ break;
97
+ case "login_shown":
98
+ emit("login_shown");
99
+ break;
100
+ case "dashboard_reached":
101
+ emit("dashboard_reached", {}, true);
102
+ break;
103
+ default: {
104
+ const _exhaustive = body;
105
+ void _exhaustive;
106
+ }
107
+ }
108
+ },
109
+ recordProductInstallStarted(product) {
110
+ emit("product_install_started", { product });
111
+ },
112
+ recordProductInstallCompleted(product) {
113
+ emit("product_install_completed", { product });
114
+ },
115
+ recordProductInstallFailed(product, stage) {
116
+ const failureStage = failureStageFromInstallStage(stage);
117
+ if (failureStage === null)
118
+ return;
119
+ emit("product_install_failed", { product, failure_stage: failureStage });
120
+ },
121
+ observeHealthReady(ready) {
122
+ if (!ready)
123
+ return;
124
+ emit("health_check_passed", {}, true);
125
+ },
126
+ observeAuthenticated(authenticated) {
127
+ if (!authenticated || sawAuthenticated)
128
+ return;
129
+ sawAuthenticated = true;
130
+ emit("login_completed", {}, true);
131
+ }
132
+ };
133
+ }
134
+ //# sourceMappingURL=funnel-telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"funnel-telemetry.js","sourceRoot":"","sources":["../../../src/daemon/installer/funnel-telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EACL,aAAa,EACb,cAAc,EAMf,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EAEpB,MAAM,8CAA8C,CAAC;AAGtD,wEAAwE;AACxE,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,oBAAoB;IACpB,eAAe;IACf,aAAa;IACb,mBAAmB;CACX,CAAC;AAIX,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC;IACjC,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;CACjE,CAAC,CAAC;AAEH,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,aAAa,EAAE,mBAAmB,CAAC,CAAC;IACzE,UAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CACxD,CAAC,CAAC;AAEH,iEAAiE;AACjE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,sBAAsB,EAAE,uBAAuB,CAAC,CAAC,CAAC;AAIpG,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAmB;IACjD,oBAAoB;IACpB,qBAAqB;IACrB,iBAAiB;IACjB,mBAAmB;CACpB,CAAC,CAAC;AAEH,MAAM,cAAc,GAAkC;IACpD,WAAW;IACX,aAAa;IACb,SAAS;IACT,qBAAqB;CACtB,CAAC;AAEF,SAAS,4BAA4B,CAAC,KAAmB;IACvD,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAQ,cAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAA4B,CAAC,CAAC,CAAC,IAAI,CAAC;AACtG,CAAC;AAyBD,SAAS,cAAc,CAAC,MAAuB;IAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAClD,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,mBAAmB,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,aAAa,CAAC,GAAwB;IAC7C,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;QACpB,iEAAiE;IACnE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAyB;IAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,EAAE,QAAQ,IAAI,cAAc,CAAC;IAC5E,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,GAAW,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACrE,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,MAAM,IAAI,GAAG,CAAC,KAA4B,EAAE,SAAuB,EAAE,EAAE,aAAa,GAAG,KAAK,EAAQ,EAAE;QACpG,aAAa,CAAC,KAAK,IAAI,EAAE;YACvB,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC/C,IAAI,aAAa,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;gBACzC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAyB,CAAC;oBAAE,OAAO;gBAC7D,MAAM,MAAM,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;gBAC9C,IAAI,sBAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAyB,CAAC;oBAAE,OAAO;YACpF,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACjE,IACE,OAAO,CAAC,IAAI;gBACZ,aAAa;gBACb,UAAU,KAAK,IAAI;gBACnB,gBAAgB,CAAC,GAAG,CAAC,KAAyB,CAAC,EAC/C,CAAC;gBACD,IAAI,CAAC;oBACH,wBAAwB,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAyB,EAAE,KAAK,CAAC,CAAC;gBACnF,CAAC;gBAAC,MAAM,CAAC;oBACP,uDAAuD;gBACzD,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO;QACL,aAAa,CAAC,IAAI;YAChB,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;gBACnB,KAAK,eAAe;oBAClB,IAAI,CAAC,eAAe,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,IAAkB,EAAE,CAAC,CAAC;oBACpE,MAAM;gBACR,KAAK,oBAAoB;oBACvB,IAAI,CAAC,oBAAoB,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;oBACrC,MAAM;gBACR,KAAK,aAAa;oBAChB,IAAI,CAAC,aAAa,CAAC,CAAC;oBACpB,MAAM;gBACR,KAAK,mBAAmB;oBACtB,IAAI,CAAC,mBAAmB,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;oBACpC,MAAM;gBACR,OAAO,CAAC,CAAC,CAAC;oBACR,MAAM,WAAW,GAAU,IAAI,CAAC;oBAChC,KAAK,WAAW,CAAC;gBACnB,CAAC;YACH,CAAC;QACH,CAAC;QAED,2BAA2B,CAAC,OAAO;YACjC,IAAI,CAAC,yBAAyB,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;QAED,6BAA6B,CAAC,OAAO;YACnC,IAAI,CAAC,2BAA2B,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;QACjD,CAAC;QAED,0BAA0B,CAAC,OAAO,EAAE,KAAK;YACvC,MAAM,YAAY,GAAG,4BAA4B,CAAC,KAAK,CAAC,CAAC;YACzD,IAAI,YAAY,KAAK,IAAI;gBAAE,OAAO;YAClC,IAAI,CAAC,wBAAwB,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,kBAAkB,CAAC,KAAK;YACtB,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,IAAI,CAAC,qBAAqB,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QAED,oBAAoB,CAAC,aAAa;YAChC,IAAI,CAAC,aAAa,IAAI,gBAAgB;gBAAE,OAAO;YAC/C,gBAAgB,GAAG,IAAI,CAAC;YACxB,IAAI,CAAC,iBAAiB,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACpC,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * PRD-009a: the installer service public surface.
3
+ *
4
+ * `server.ts` imports {@link createInstallerService} and registers its routes before the generic
5
+ * `/api/*` BFF proxy. Everything else here is exported for the installer test suites.
6
+ */
7
+ export { createInstallerService } from "./routes.js";
8
+ export type { InstallerService, InstallerServiceOptions } from "./routes.js";
9
+ export { createInstallerConfig } from "./config.js";
10
+ export type { InstallerConfig } from "./config.js";
11
+ export type { SpawnFn, SpawnOutcome, RawSpawn } from "./spawn.js";
12
+ export { createNodeSpawn } from "./spawn.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * PRD-009a: the installer service public surface.
3
+ *
4
+ * `server.ts` imports {@link createInstallerService} and registers its routes before the generic
5
+ * `/api/*` BFF proxy. Everything else here is exported for the installer test suites.
6
+ */
7
+ export { createInstallerService } from "./routes.js";
8
+ export { createInstallerConfig } from "./config.js";
9
+ export { createNodeSpawn } from "./spawn.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/daemon/installer/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * PRD-009a: the in-memory install state machine, SSE broadcast, and child-process orchestration
3
+ * (is-AC-11 through is-AC-17).
4
+ *
5
+ * State is daemon-session scoped, not persisted (the parent PRD adds no store). Each product moves
6
+ * through idle -> in_progress -> installed | failed. Progress is broadcast to any number of SSE
7
+ * subscribers as staged events drawn from a closed set, NEVER a synthesized percentage (is-AC-12).
8
+ * Stages are derived from real signals: `resolving` (target pinned), `downloading` (npm spawned),
9
+ * `linking` (npm exited 0), `registering_service` (the product's own verb spawned), then
10
+ * `completed` / `failed` from the verb's exit code. An install runs to its terminal state even if
11
+ * every subscriber disconnects (is-AC-14): the work is owned by daemon-held state, not a browser tab.
12
+ */
13
+ import type { InstallableProduct, InstallError, InstallStage, ProgressEvent } from "../../shared/onboarding-types.js";
14
+ import type { InstallerConfig } from "./config.js";
15
+ /** A resolved, shape-validated npm install target (from `manifest.ts`, never from the request). */
16
+ export interface InstallTarget {
17
+ readonly packageName: string;
18
+ readonly version: string;
19
+ readonly target: string;
20
+ }
21
+ /** The lifecycle status of one product's install. */
22
+ export type InstallStatus = "idle" | "in_progress" | "installed" | "failed";
23
+ /** An SSE consumer of one product's progress stream. */
24
+ export interface ProgressSubscriber {
25
+ send(event: ProgressEvent): void;
26
+ close(): void;
27
+ }
28
+ /** The result of a `begin` call: a fresh install started, or attachment to an in-flight one (is-AC-16). */
29
+ export type BeginResult = "started" | "attached";
30
+ /** Optional funnel hooks; when absent, no telemetry is emitted. */
31
+ export interface InstallStateFunnelHooks {
32
+ readonly onInstallStarted?: (product: InstallableProduct) => void;
33
+ readonly onInstallCompleted?: (product: InstallableProduct) => void;
34
+ readonly onInstallFailed?: (product: InstallableProduct, stage: InstallStage) => void;
35
+ }
36
+ /** A read-only snapshot of one product's install state, consumed by detection (is-AC-2). */
37
+ export interface ProductStateSnapshot {
38
+ readonly status: InstallStatus;
39
+ readonly currentStage: InstallStage | null;
40
+ readonly error?: InstallError;
41
+ }
42
+ /** The installer's session state: begin/attach installs, subscribe to progress, record funnel events. */
43
+ export interface InstallStateStore {
44
+ detectState(product: InstallableProduct): ProductStateSnapshot;
45
+ begin(product: InstallableProduct, target: InstallTarget): BeginResult;
46
+ subscribe(product: InstallableProduct, subscriber: ProgressSubscriber): () => void;
47
+ /** The in-flight install promise for a product (resolved when none), for test synchronization. */
48
+ settled(product: InstallableProduct): Promise<void>;
49
+ }
50
+ export declare function createInstallStateStore(config: InstallerConfig, hooks?: InstallStateFunnelHooks): InstallStateStore;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * PRD-009a: the in-memory install state machine, SSE broadcast, and child-process orchestration
3
+ * (is-AC-11 through is-AC-17).
4
+ *
5
+ * State is daemon-session scoped, not persisted (the parent PRD adds no store). Each product moves
6
+ * through idle -> in_progress -> installed | failed. Progress is broadcast to any number of SSE
7
+ * subscribers as staged events drawn from a closed set, NEVER a synthesized percentage (is-AC-12).
8
+ * Stages are derived from real signals: `resolving` (target pinned), `downloading` (npm spawned),
9
+ * `linking` (npm exited 0), `registering_service` (the product's own verb spawned), then
10
+ * `completed` / `failed` from the verb's exit code. An install runs to its terminal state even if
11
+ * every subscriber disconnects (is-AC-14): the work is owned by daemon-held state, not a browser tab.
12
+ */
13
+ import { globalNodeModulesDir, locateNpmCliJs, resolvePackageBinJs } from "./bin-resolver.js";
14
+ import { productProfile } from "./products.js";
15
+ /** Bound a spawn's error label + exit code + stderr tail into a single truthful summary (is-AC-17). */
16
+ function summarizeFailure(label, code, stderrTail) {
17
+ const trimmed = stderrTail.trim();
18
+ const suffix = trimmed.length > 0 ? `: ${trimmed}` : "";
19
+ return `${label} exited with code ${code ?? "null"}${suffix}`;
20
+ }
21
+ export function createInstallStateStore(config, hooks = {}) {
22
+ const states = new Map();
23
+ const stateFor = (product) => {
24
+ let state = states.get(product);
25
+ if (state === undefined) {
26
+ state = { status: "idle", currentStage: null, error: undefined, subscribers: new Set(), inFlight: null };
27
+ states.set(product, state);
28
+ }
29
+ return state;
30
+ };
31
+ const broadcast = (state, event) => {
32
+ for (const subscriber of state.subscribers) {
33
+ subscriber.send(event);
34
+ }
35
+ };
36
+ const closeSubscribers = (state) => {
37
+ for (const subscriber of state.subscribers) {
38
+ subscriber.close();
39
+ }
40
+ state.subscribers.clear();
41
+ };
42
+ const setStage = (state, stage, detail) => {
43
+ state.currentStage = stage;
44
+ broadcast(state, detail === undefined ? { stage } : { stage, detail });
45
+ };
46
+ const complete = (state, product) => {
47
+ state.status = "installed";
48
+ state.error = undefined;
49
+ setStage(state, "completed");
50
+ hooks.onInstallCompleted?.(product);
51
+ closeSubscribers(state);
52
+ };
53
+ const fail = (state, stage, summary, product) => {
54
+ state.status = "failed";
55
+ state.error = { stage, summary };
56
+ state.currentStage = "failed";
57
+ hooks.onInstallFailed?.(product, stage);
58
+ broadcast(state, { stage: "failed", detail: summary });
59
+ closeSubscribers(state);
60
+ };
61
+ const performInstall = async (product, target, state) => {
62
+ // Yield once so the synchronously-set `resolving` stage is observable in order before the
63
+ // `downloading` transition (the install is already owned by daemon state at this point).
64
+ await Promise.resolve();
65
+ try {
66
+ const npmCli = locateNpmCliJs(config);
67
+ if (npmCli === null) {
68
+ fail(state, "downloading", "npm executable could not be located", product);
69
+ return;
70
+ }
71
+ setStage(state, "downloading");
72
+ const npm = await config.spawn(config.execPath, [npmCli, "install", "-g", target.target]);
73
+ if (npm.code !== 0) {
74
+ fail(state, "downloading", summarizeFailure("npm install", npm.code, npm.stderrTail), product);
75
+ return;
76
+ }
77
+ setStage(state, "linking");
78
+ const profile = productProfile(product);
79
+ const prefix = await config.resolveNpmPrefix();
80
+ const verbBin = prefix === null
81
+ ? null
82
+ : resolvePackageBinJs(config, globalNodeModulesDir(prefix, config.platform), target.packageName, profile.binName);
83
+ if (verbBin === null) {
84
+ fail(state, "registering_service", `could not resolve the ${profile.binName} bin to run its registration verb`, product);
85
+ return;
86
+ }
87
+ setStage(state, "registering_service");
88
+ const registration = await config.spawn(config.execPath, [verbBin, ...profile.registrationVerb]);
89
+ if (registration.code !== 0) {
90
+ const label = `${profile.binName} ${profile.registrationVerb.join(" ")}`;
91
+ fail(state, "registering_service", summarizeFailure(label, registration.code, registration.stderrTail), product);
92
+ return;
93
+ }
94
+ complete(state, product);
95
+ }
96
+ catch (error) {
97
+ const stage = state.currentStage === "registering_service" ? "registering_service" : "downloading";
98
+ fail(state, stage, error instanceof Error ? error.message : String(error), product);
99
+ }
100
+ };
101
+ return {
102
+ detectState(product) {
103
+ const state = stateFor(product);
104
+ return { status: state.status, currentStage: state.currentStage, error: state.error };
105
+ },
106
+ begin(product, target) {
107
+ const state = stateFor(product);
108
+ // is-AC-16: a second concurrent request attaches to the in-flight install; only one child runs.
109
+ if (state.status === "in_progress")
110
+ return "attached";
111
+ // is-AC-17: a prior failure is retryable; reset and run again.
112
+ state.status = "in_progress";
113
+ state.error = undefined;
114
+ setStage(state, "resolving");
115
+ hooks.onInstallStarted?.(product);
116
+ state.inFlight = performInstall(product, target, state);
117
+ return "started";
118
+ },
119
+ subscribe(product, subscriber) {
120
+ const state = stateFor(product);
121
+ state.subscribers.add(subscriber);
122
+ // is-AC-14: immediately replay the CURRENT stage so a re-attached client catches up.
123
+ if (state.currentStage !== null) {
124
+ const detail = state.currentStage === "failed" ? state.error?.summary : undefined;
125
+ subscriber.send(detail === undefined ? { stage: state.currentStage } : { stage: state.currentStage, detail });
126
+ }
127
+ // If the install has already reached a terminal state, end this late subscriber's stream.
128
+ if (state.status === "installed" || state.status === "failed") {
129
+ state.subscribers.delete(subscriber);
130
+ subscriber.close();
131
+ return () => undefined;
132
+ }
133
+ return () => {
134
+ state.subscribers.delete(subscriber);
135
+ };
136
+ },
137
+ settled(product) {
138
+ return stateFor(product).inFlight ?? Promise.resolve();
139
+ }
140
+ };
141
+ }
142
+ //# sourceMappingURL=install-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-state.js","sourceRoot":"","sources":["../../../src/daemon/installer/install-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ {
2
+ "manifestVersion": "0.2.1",
3
+ "products": {
4
+ "honeycomb": {
5
+ "version": "0.2.1",
6
+ "packageName": "@legioncodeinc/honeycomb",
7
+ "published": true
8
+ },
9
+ "doctor": {
10
+ "version": "0.2.1",
11
+ "packageName": "@legioncodeinc/doctor",
12
+ "published": true
13
+ },
14
+ "hive": {
15
+ "version": "0.2.1",
16
+ "packageName": "@legioncodeinc/hive",
17
+ "published": true
18
+ },
19
+ "nectar": {
20
+ "version": "0.1.1",
21
+ "packageName": "@legioncodeinc/nectar",
22
+ "published": true
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PRD-009a: server-side `packageName@version` resolution from the fleet manifest (is-AC-4/5).
3
+ *
4
+ * The install target is NEVER taken from the request. It is resolved from `hive-release.json`:
5
+ * fetched once per daemon session from the raw URL (bounded timeout), cached in memory, with a
6
+ * build-time bundled snapshot as the offline fallback. Before any resolved value is used it must
7
+ * pass a strict shape check (scoped-npm-name regex + semver regex, mirroring install.sh's
8
+ * `npm_package_name_is_safe` / `semver_is_safe`), so a malformed or tampered field is refused
9
+ * (is-AC-5) rather than degrading into an unpinned `npm i -g <name>@latest`.
10
+ *
11
+ * The imported snapshot is the SHIP-TIME manifest: the version set the running hive shipped with.
12
+ * It is only consulted when the network fetch fails, so an offline-ish first run still pins.
13
+ */
14
+ import { z } from "zod";
15
+ import type { InstallableProduct } from "../../shared/onboarding-types.js";
16
+ import type { InstallerConfig } from "./config.js";
17
+ declare const ManifestSchema: z.ZodObject<{
18
+ manifestVersion: z.ZodString;
19
+ products: z.ZodRecord<z.ZodString, z.ZodObject<{
20
+ version: z.ZodString;
21
+ packageName: z.ZodString;
22
+ published: z.ZodBoolean;
23
+ }, z.core.$strip>>;
24
+ }, z.core.$strip>;
25
+ export type ParsedManifest = z.infer<typeof ManifestSchema>;
26
+ /** The outcome of resolving an install target for a product. */
27
+ export type TargetResolution = {
28
+ readonly kind: "ok";
29
+ readonly packageName: string;
30
+ readonly version: string;
31
+ readonly target: string;
32
+ } | {
33
+ readonly kind: "unpublished";
34
+ } | {
35
+ readonly kind: "manifest_unresolved";
36
+ };
37
+ /** The manifest resolver: a memoized manifest plus a per-product target resolver. */
38
+ export interface ManifestResolver {
39
+ resolve(product: InstallableProduct): Promise<TargetResolution>;
40
+ }
41
+ /**
42
+ * Build a session-scoped manifest resolver. The manifest is fetched at most once (memoized on the
43
+ * first call); a network failure falls back to the bundled snapshot. Returns `manifest_unresolved`
44
+ * only when neither source yields a shape-valid entry for the requested product.
45
+ */
46
+ export declare function createManifestResolver(config: InstallerConfig): ManifestResolver;
47
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * PRD-009a: server-side `packageName@version` resolution from the fleet manifest (is-AC-4/5).
3
+ *
4
+ * The install target is NEVER taken from the request. It is resolved from `hive-release.json`:
5
+ * fetched once per daemon session from the raw URL (bounded timeout), cached in memory, with a
6
+ * build-time bundled snapshot as the offline fallback. Before any resolved value is used it must
7
+ * pass a strict shape check (scoped-npm-name regex + semver regex, mirroring install.sh's
8
+ * `npm_package_name_is_safe` / `semver_is_safe`), so a malformed or tampered field is refused
9
+ * (is-AC-5) rather than degrading into an unpinned `npm i -g <name>@latest`.
10
+ *
11
+ * The imported snapshot is the SHIP-TIME manifest: the version set the running hive shipped with.
12
+ * It is only consulted when the network fetch fails, so an offline-ish first run still pins.
13
+ */
14
+ import { z } from "zod";
15
+ // The ship-time snapshot fallback (is-AC-5). Comment-free JSON copied from the superproject
16
+ // `hive-release.json`; consulted only when the network manifest fetch fails.
17
+ import manifestSnapshot from "./manifest-snapshot.json" with { type: "json" };
18
+ /** A conservative npm package-name allowlist (lowercase / digits / `.` `_` `-`, optional `@scope/`). */
19
+ const NPM_NAME_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
20
+ /** digits.digits.digits with an optional `-prerelease` / `+build` drawn from a safe charset. */
21
+ const SEMVER_RE = /^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.+-]+)?$/;
22
+ const ProductEntrySchema = z.object({
23
+ version: z.string(),
24
+ packageName: z.string(),
25
+ published: z.boolean()
26
+ });
27
+ const ManifestSchema = z.object({
28
+ manifestVersion: z.string(),
29
+ products: z.record(z.string(), ProductEntrySchema)
30
+ });
31
+ function validateSnapshot() {
32
+ const parsed = ManifestSchema.safeParse(manifestSnapshot);
33
+ return parsed.success ? parsed.data : null;
34
+ }
35
+ async function fetchManifestFromUrl(config, url) {
36
+ try {
37
+ const response = await config.manifestFetch(url, {
38
+ signal: AbortSignal.timeout(config.manifestTimeoutMs)
39
+ });
40
+ if (!response.ok)
41
+ return null;
42
+ let parsedJson;
43
+ try {
44
+ parsedJson = await response.json();
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ const parsed = ManifestSchema.safeParse(parsedJson);
50
+ return parsed.success ? parsed.data : null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ async function fetchNetworkManifest(config) {
57
+ const primary = await fetchManifestFromUrl(config, config.manifestUrl);
58
+ if (primary !== null)
59
+ return primary;
60
+ if (config.manifestFallbackUrl !== config.manifestUrl) {
61
+ const fallback = await fetchManifestFromUrl(config, config.manifestFallbackUrl);
62
+ if (fallback !== null)
63
+ return fallback;
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Build a session-scoped manifest resolver. The manifest is fetched at most once (memoized on the
69
+ * first call); a network failure falls back to the bundled snapshot. Returns `manifest_unresolved`
70
+ * only when neither source yields a shape-valid entry for the requested product.
71
+ */
72
+ export function createManifestResolver(config) {
73
+ let manifestPromise = null;
74
+ const getManifest = () => {
75
+ if (manifestPromise === null) {
76
+ manifestPromise = fetchNetworkManifest(config).then((network) => network ?? validateSnapshot());
77
+ }
78
+ return manifestPromise;
79
+ };
80
+ return {
81
+ async resolve(product) {
82
+ const manifest = await getManifest();
83
+ if (manifest === null)
84
+ return { kind: "manifest_unresolved" };
85
+ const entry = manifest.products[product];
86
+ if (entry === undefined)
87
+ return { kind: "manifest_unresolved" };
88
+ // Refuse a tampered/malformed field rather than ever reaching npm with an unsafe target.
89
+ if (!NPM_NAME_RE.test(entry.packageName) || !SEMVER_RE.test(entry.version)) {
90
+ return { kind: "manifest_unresolved" };
91
+ }
92
+ if (!entry.published)
93
+ return { kind: "unpublished" };
94
+ return {
95
+ kind: "ok",
96
+ packageName: entry.packageName,
97
+ version: entry.version,
98
+ target: `${entry.packageName}@${entry.version}`
99
+ };
100
+ }
101
+ };
102
+ }
103
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../../src/daemon/installer/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * PRD-009a: the hard-coded product allowlist and per-product registration verbs (is-AC-3/13).
3
+ *
4
+ * This is the single source of truth for which slugs exist, their npm bin names, and the OWN
5
+ * registration verb each product runs post-install, exactly as honeycomb's `install.sh` does today
6
+ * (`doctor install-service`, `honeycomb install`, `nectar install`). The install endpoint only ever
7
+ * acts on a slug present in {@link INSTALLABLE_PRODUCTS}; `hive` is deliberately absent (it is the
8
+ * running daemon, never a portal install target).
9
+ */
10
+ import type { InstallableProduct, ProductSlug } from "../../shared/onboarding-types.js";
11
+ /** The four known product slugs (is-AC-3). The install allowlist is the installable subset below. */
12
+ export declare const PRODUCT_SLUGS: readonly ["honeycomb", "doctor", "hive", "nectar"];
13
+ /**
14
+ * The npm package name for each slug, used as LOCAL DETECTION evidence (the folder name under the
15
+ * global node_modules). This is deliberately independent of the manifest so detection works before
16
+ * any manifest is fetched (is-AC-1); the manifest remains the sole authority for INSTALL targets.
17
+ */
18
+ export declare const PRODUCT_PACKAGES: Record<ProductSlug, string>;
19
+ /** The three portal-installable products; `hive` is excluded (not installable, 400). */
20
+ export declare const INSTALLABLE_PRODUCTS: readonly ["doctor", "honeycomb", "nectar"];
21
+ /** Per-product static facts: the npm bin name and the argv of its own post-install registration verb. */
22
+ export interface ProductProfile {
23
+ /** The npm bin name (equals the slug for every fleet product). */
24
+ readonly binName: string;
25
+ /** The registration verb argv run after a successful npm install (is-AC-13). */
26
+ readonly registrationVerb: readonly string[];
27
+ }
28
+ /** True for one of the four known slugs (is-AC-3 allowlist membership). */
29
+ export declare function isProductSlug(value: string): value is ProductSlug;
30
+ /** True for a portal-installable product (`hive` is excluded). */
31
+ export declare function isInstallableProduct(value: string): value is InstallableProduct;
32
+ /** The static profile (bin name + registration verb) for an installable product. */
33
+ export declare function productProfile(product: InstallableProduct): ProductProfile;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * PRD-009a: the hard-coded product allowlist and per-product registration verbs (is-AC-3/13).
3
+ *
4
+ * This is the single source of truth for which slugs exist, their npm bin names, and the OWN
5
+ * registration verb each product runs post-install, exactly as honeycomb's `install.sh` does today
6
+ * (`doctor install-service`, `honeycomb install`, `nectar install`). The install endpoint only ever
7
+ * acts on a slug present in {@link INSTALLABLE_PRODUCTS}; `hive` is deliberately absent (it is the
8
+ * running daemon, never a portal install target).
9
+ */
10
+ /** The four known product slugs (is-AC-3). The install allowlist is the installable subset below. */
11
+ export const PRODUCT_SLUGS = ["honeycomb", "doctor", "hive", "nectar"];
12
+ /**
13
+ * The npm package name for each slug, used as LOCAL DETECTION evidence (the folder name under the
14
+ * global node_modules). This is deliberately independent of the manifest so detection works before
15
+ * any manifest is fetched (is-AC-1); the manifest remains the sole authority for INSTALL targets.
16
+ */
17
+ export const PRODUCT_PACKAGES = {
18
+ honeycomb: "@legioncodeinc/honeycomb",
19
+ doctor: "@legioncodeinc/doctor",
20
+ hive: "@legioncodeinc/hive",
21
+ nectar: "@legioncodeinc/nectar"
22
+ };
23
+ /** The three portal-installable products; `hive` is excluded (not installable, 400). */
24
+ export const INSTALLABLE_PRODUCTS = ["doctor", "honeycomb", "nectar"];
25
+ const PROFILES = {
26
+ doctor: { binName: "doctor", registrationVerb: ["install-service"] },
27
+ honeycomb: { binName: "honeycomb", registrationVerb: ["install"] },
28
+ nectar: { binName: "nectar", registrationVerb: ["install"] }
29
+ };
30
+ /** True for one of the four known slugs (is-AC-3 allowlist membership). */
31
+ export function isProductSlug(value) {
32
+ return PRODUCT_SLUGS.includes(value);
33
+ }
34
+ /** True for a portal-installable product (`hive` is excluded). */
35
+ export function isInstallableProduct(value) {
36
+ return INSTALLABLE_PRODUCTS.includes(value);
37
+ }
38
+ /** The static profile (bin name + registration verb) for an installable product. */
39
+ export function productProfile(product) {
40
+ return PROFILES[product];
41
+ }
42
+ //# sourceMappingURL=products.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products.js","sourceRoot":"","sources":["../../../src/daemon/installer/products.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,qGAAqG;AACrG,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAgC;IAC3D,SAAS,EAAE,0BAA0B;IACrC,MAAM,EAAE,uBAAuB;IAC/B,IAAI,EAAE,qBAAqB;IAC3B,MAAM,EAAE,uBAAuB;CAChC,CAAC;AAEF,wFAAwF;AACxF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAU/E,MAAM,QAAQ,GAA+C;IAC3D,MAAM,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,iBAAiB,CAAC,EAAE;IACpE,SAAS,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC,SAAS,CAAC,EAAE;IAClE,MAAM,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,SAAS,CAAC,EAAE;CAC7D,CAAC;AAEF,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAQ,aAAmC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAQ,oBAA0C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACrE,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAAC,OAA2B;IACxD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC3B,CAAC"}