@takodotid/circuit 0.34.2 → 0.35.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 (240) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11347 -9018
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +17 -11
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -693
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -9,6 +9,8 @@ export type StaticRoute<I extends string, F extends string> = {
9
9
  interface?: I;
10
10
  /** Discard matching traffic. The usual way to originate an aggregate. */
11
11
  blackhole?: true;
12
+ /** Withdraw the route while BFD finds `via` dead, and put it back once `via` answers again. Needs `via`. */
13
+ bfd?: true;
12
14
  /** Preference against other routes to the same prefix. Lower wins. 1 when absent. */
13
15
  distance?: number;
14
16
  /** The VRF the route is in. The default table when absent. */
@@ -69,6 +71,13 @@ export type Bgp<P extends string, G extends string> = {
69
71
  /** Sessions keyed by name. */
70
72
  neighbors?: Record<string, Neighbor<P, G>>;
71
73
  };
74
+ /** How fast BFD notices a dead neighbor: after `multiplier` packets in a row are missed, so in `interval` times `multiplier` milliseconds. */
75
+ export type Bfd = {
76
+ /** Milliseconds between the packets this device sends, and the shortest gap it asks of the neighbor. */
77
+ interval: number;
78
+ /** How many packets in a row may be missed before the neighbor is declared down. */
79
+ multiplier: number;
80
+ };
72
81
  /** A route origin validation server, RFC 8210. */
73
82
  export type RpkiServer = {
74
83
  /** Where the validator listens. */
@@ -89,12 +98,14 @@ export type OspfInterface = {
89
98
  /** Detect a dead neighbor in under a second with BFD. */
90
99
  bfd?: boolean;
91
100
  };
101
+ /** An OSPF area ID, written as four numbers with dots between them, for example `0.0.0.0` for the backbone. */
102
+ export type OspfAreaId = `${number}.${number}.${number}.${number}`;
92
103
  /** OSPF, version 2 for IPv4 and version 3 for IPv6. */
93
104
  export type Ospf<I extends string> = {
94
105
  /** Which versions run. IPv4 when absent. */
95
106
  families?: readonly Family[];
96
107
  /** Areas keyed by area ID, for example `0.0.0.0`, each with the interfaces in it. */
97
- areas: Record<string, {
108
+ areas: Record<OspfAreaId, {
98
109
  interfaces: Partial<Record<I, OspfInterface>>;
99
110
  }>;
100
111
  /** Routes from other sources announced into OSPF. */
@@ -112,4 +123,6 @@ export type Routing<P extends string, G extends string, I extends string, F exte
112
123
  ospf?: Ospf<I>;
113
124
  /** Validators answering route origin validation. */
114
125
  rpki?: readonly RpkiServer[];
126
+ /** BFD timers for every BFD session on the device: BGP, OSPF and static routes. The platform's own when absent; Junos has none, so Circuit sends every 300 ms there. Both ends of a link should agree. */
127
+ bfd?: Bfd;
115
128
  };
@@ -1,8 +1,13 @@
1
- import type { IP, Prefix, Secret, Severity } from "./common";
1
+ import type { IP, MacAddress, Prefix, Secret, Severity } from "./common";
2
+ import type { TimeZone } from "./time-zones";
3
+ /** A software release track, as RouterOS names them. `long-term` changes least; `development` is the newest and least tested. */
4
+ export type ReleaseChannel = "long-term" | "stable" | "testing" | "development";
5
+ /** How bad an alarm is, most severe first, as ITU-T X.733 ranks them. */
6
+ export type AlarmSeverity = "critical" | "major" | "minor" | "warning";
2
7
  /** Device-wide settings. */
3
8
  export type System = {
4
- /** IANA zone, for example `Asia/Jakarta`. UTC when absent. */
5
- timezone?: string;
9
+ /** IANA zone, for example `Asia/Jakarta`. The editor suggests every name. UTC when absent. */
10
+ timezone?: TimeZone;
6
11
  /** Shown at login. */
7
12
  banner?: string;
8
13
  /** Resolvers the device uses. */
@@ -39,8 +44,12 @@ export type System = {
39
44
  level: Severity;
40
45
  }[];
41
46
  };
42
- /** Software release track the device updates from. */
43
- release_channel?: string;
47
+ /** Software release track the device updates from. `stable` when absent. */
48
+ release_channel?: ReleaseChannel;
49
+ /** Alarms raised at another severity than the platform's own, keyed by the alarm's name as the platform spells it, such as `hwOpticalUnAuthorized` on VRP. */
50
+ alarms?: Record<string, {
51
+ severity: AlarmSeverity;
52
+ }>;
44
53
  /** Read optic temperature, power and voltage. */
45
54
  transceiver_monitoring?: boolean;
46
55
  /** Host behaviour of the IP stack. */
@@ -152,7 +161,8 @@ export type DhcpServer<I extends string> = {
152
161
  /** Addresses fixed to a MAC. */
153
162
  reservations?: readonly {
154
163
  address: IP;
155
- mac: string;
164
+ /** The client's MAC address. */
165
+ mac: MacAddress;
156
166
  /** Whose address it is. */
157
167
  description?: string;
158
168
  }[];
@@ -0,0 +1,2 @@
1
+ /** An IANA time zone name, such as `Asia/Jakarta`, or `UTC`. */
2
+ export type TimeZone = "UTC" | "Africa/Abidjan" | "Africa/Accra" | "Africa/Addis_Ababa" | "Africa/Algiers" | "Africa/Asmara" | "Africa/Asmera" | "Africa/Bamako" | "Africa/Bangui" | "Africa/Banjul" | "Africa/Bissau" | "Africa/Blantyre" | "Africa/Brazzaville" | "Africa/Bujumbura" | "Africa/Cairo" | "Africa/Casablanca" | "Africa/Ceuta" | "Africa/Conakry" | "Africa/Dakar" | "Africa/Dar_es_Salaam" | "Africa/Djibouti" | "Africa/Douala" | "Africa/El_Aaiun" | "Africa/Freetown" | "Africa/Gaborone" | "Africa/Harare" | "Africa/Johannesburg" | "Africa/Juba" | "Africa/Kampala" | "Africa/Khartoum" | "Africa/Kigali" | "Africa/Kinshasa" | "Africa/Lagos" | "Africa/Libreville" | "Africa/Lome" | "Africa/Luanda" | "Africa/Lubumbashi" | "Africa/Lusaka" | "Africa/Malabo" | "Africa/Maputo" | "Africa/Maseru" | "Africa/Mbabane" | "Africa/Mogadishu" | "Africa/Monrovia" | "Africa/Nairobi" | "Africa/Ndjamena" | "Africa/Niamey" | "Africa/Nouakchott" | "Africa/Ouagadougou" | "Africa/Porto-Novo" | "Africa/Sao_Tome" | "Africa/Tripoli" | "Africa/Tunis" | "Africa/Windhoek" | "America/Adak" | "America/Anchorage" | "America/Anguilla" | "America/Antigua" | "America/Araguaina" | "America/Argentina/Buenos_Aires" | "America/Argentina/Catamarca" | "America/Argentina/Cordoba" | "America/Argentina/Jujuy" | "America/Argentina/La_Rioja" | "America/Argentina/Mendoza" | "America/Argentina/Rio_Gallegos" | "America/Argentina/Salta" | "America/Argentina/San_Juan" | "America/Argentina/San_Luis" | "America/Argentina/Tucuman" | "America/Argentina/Ushuaia" | "America/Aruba" | "America/Asuncion" | "America/Atikokan" | "America/Bahia" | "America/Bahia_Banderas" | "America/Barbados" | "America/Belem" | "America/Belize" | "America/Blanc-Sablon" | "America/Boa_Vista" | "America/Bogota" | "America/Boise" | "America/Buenos_Aires" | "America/Cambridge_Bay" | "America/Campo_Grande" | "America/Cancun" | "America/Caracas" | "America/Catamarca" | "America/Cayenne" | "America/Cayman" | "America/Chicago" | "America/Chihuahua" | "America/Ciudad_Juarez" | "America/Coral_Harbour" | "America/Cordoba" | "America/Costa_Rica" | "America/Creston" | "America/Cuiaba" | "America/Curacao" | "America/Danmarkshavn" | "America/Dawson" | "America/Dawson_Creek" | "America/Denver" | "America/Detroit" | "America/Dominica" | "America/Edmonton" | "America/Eirunepe" | "America/El_Salvador" | "America/Fort_Nelson" | "America/Fortaleza" | "America/Glace_Bay" | "America/Godthab" | "America/Goose_Bay" | "America/Grand_Turk" | "America/Grenada" | "America/Guadeloupe" | "America/Guatemala" | "America/Guayaquil" | "America/Guyana" | "America/Halifax" | "America/Havana" | "America/Hermosillo" | "America/Indiana/Indianapolis" | "America/Indiana/Knox" | "America/Indiana/Marengo" | "America/Indiana/Petersburg" | "America/Indiana/Tell_City" | "America/Indiana/Vevay" | "America/Indiana/Vincennes" | "America/Indiana/Winamac" | "America/Indianapolis" | "America/Inuvik" | "America/Iqaluit" | "America/Jamaica" | "America/Jujuy" | "America/Juneau" | "America/Kentucky/Louisville" | "America/Kentucky/Monticello" | "America/Kralendijk" | "America/La_Paz" | "America/Lima" | "America/Los_Angeles" | "America/Louisville" | "America/Lower_Princes" | "America/Maceio" | "America/Managua" | "America/Manaus" | "America/Marigot" | "America/Martinique" | "America/Matamoros" | "America/Mazatlan" | "America/Mendoza" | "America/Menominee" | "America/Merida" | "America/Metlakatla" | "America/Mexico_City" | "America/Miquelon" | "America/Moncton" | "America/Monterrey" | "America/Montevideo" | "America/Montserrat" | "America/Nassau" | "America/New_York" | "America/Nipigon" | "America/Nome" | "America/Noronha" | "America/North_Dakota/Beulah" | "America/North_Dakota/Center" | "America/North_Dakota/New_Salem" | "America/Nuuk" | "America/Ojinaga" | "America/Panama" | "America/Pangnirtung" | "America/Paramaribo" | "America/Phoenix" | "America/Port-au-Prince" | "America/Port_of_Spain" | "America/Porto_Velho" | "America/Puerto_Rico" | "America/Punta_Arenas" | "America/Rainy_River" | "America/Rankin_Inlet" | "America/Recife" | "America/Regina" | "America/Resolute" | "America/Rio_Branco" | "America/Santa_Isabel" | "America/Santarem" | "America/Santiago" | "America/Santo_Domingo" | "America/Sao_Paulo" | "America/Scoresbysund" | "America/Sitka" | "America/St_Barthelemy" | "America/St_Johns" | "America/St_Kitts" | "America/St_Lucia" | "America/St_Thomas" | "America/St_Vincent" | "America/Swift_Current" | "America/Tegucigalpa" | "America/Thule" | "America/Thunder_Bay" | "America/Tijuana" | "America/Toronto" | "America/Tortola" | "America/Vancouver" | "America/Whitehorse" | "America/Winnipeg" | "America/Yakutat" | "America/Yellowknife" | "Antarctica/Casey" | "Antarctica/Davis" | "Antarctica/DumontDUrville" | "Antarctica/Macquarie" | "Antarctica/Mawson" | "Antarctica/McMurdo" | "Antarctica/Palmer" | "Antarctica/Rothera" | "Antarctica/Syowa" | "Antarctica/Troll" | "Antarctica/Vostok" | "Arctic/Longyearbyen" | "Asia/Aden" | "Asia/Almaty" | "Asia/Amman" | "Asia/Anadyr" | "Asia/Aqtau" | "Asia/Aqtobe" | "Asia/Ashgabat" | "Asia/Atyrau" | "Asia/Baghdad" | "Asia/Bahrain" | "Asia/Baku" | "Asia/Bangkok" | "Asia/Barnaul" | "Asia/Beirut" | "Asia/Bishkek" | "Asia/Brunei" | "Asia/Calcutta" | "Asia/Chita" | "Asia/Choibalsan" | "Asia/Chongqing" | "Asia/Colombo" | "Asia/Damascus" | "Asia/Dhaka" | "Asia/Dili" | "Asia/Dubai" | "Asia/Dushanbe" | "Asia/Famagusta" | "Asia/Gaza" | "Asia/Hebron" | "Asia/Ho_Chi_Minh" | "Asia/Hong_Kong" | "Asia/Hovd" | "Asia/Irkutsk" | "Asia/Jakarta" | "Asia/Jayapura" | "Asia/Jerusalem" | "Asia/Kabul" | "Asia/Kamchatka" | "Asia/Karachi" | "Asia/Kathmandu" | "Asia/Katmandu" | "Asia/Khandyga" | "Asia/Kolkata" | "Asia/Krasnoyarsk" | "Asia/Kuala_Lumpur" | "Asia/Kuching" | "Asia/Kuwait" | "Asia/Macau" | "Asia/Magadan" | "Asia/Makassar" | "Asia/Manila" | "Asia/Muscat" | "Asia/Nicosia" | "Asia/Novokuznetsk" | "Asia/Novosibirsk" | "Asia/Omsk" | "Asia/Oral" | "Asia/Phnom_Penh" | "Asia/Pontianak" | "Asia/Pyongyang" | "Asia/Qatar" | "Asia/Qostanay" | "Asia/Qyzylorda" | "Asia/Rangoon" | "Asia/Riyadh" | "Asia/Saigon" | "Asia/Sakhalin" | "Asia/Samarkand" | "Asia/Seoul" | "Asia/Shanghai" | "Asia/Singapore" | "Asia/Srednekolymsk" | "Asia/Taipei" | "Asia/Tashkent" | "Asia/Tbilisi" | "Asia/Tehran" | "Asia/Thimphu" | "Asia/Tokyo" | "Asia/Tomsk" | "Asia/Ulaanbaatar" | "Asia/Urumqi" | "Asia/Ust-Nera" | "Asia/Vientiane" | "Asia/Vladivostok" | "Asia/Yakutsk" | "Asia/Yangon" | "Asia/Yekaterinburg" | "Asia/Yerevan" | "Atlantic/Azores" | "Atlantic/Bermuda" | "Atlantic/Canary" | "Atlantic/Cape_Verde" | "Atlantic/Faeroe" | "Atlantic/Faroe" | "Atlantic/Madeira" | "Atlantic/Reykjavik" | "Atlantic/South_Georgia" | "Atlantic/St_Helena" | "Atlantic/Stanley" | "Australia/Adelaide" | "Australia/Brisbane" | "Australia/Broken_Hill" | "Australia/Canberra" | "Australia/Currie" | "Australia/Darwin" | "Australia/Eucla" | "Australia/Hobart" | "Australia/Lindeman" | "Australia/Lord_Howe" | "Australia/Melbourne" | "Australia/Perth" | "Australia/Sydney" | "Etc/GMT" | "Etc/GMT+1" | "Etc/GMT+10" | "Etc/GMT+11" | "Etc/GMT+12" | "Etc/GMT+2" | "Etc/GMT+3" | "Etc/GMT+4" | "Etc/GMT+5" | "Etc/GMT+6" | "Etc/GMT+7" | "Etc/GMT+8" | "Etc/GMT+9" | "Etc/GMT-1" | "Etc/GMT-10" | "Etc/GMT-11" | "Etc/GMT-12" | "Etc/GMT-13" | "Etc/GMT-14" | "Etc/GMT-2" | "Etc/GMT-3" | "Etc/GMT-4" | "Etc/GMT-5" | "Etc/GMT-6" | "Etc/GMT-7" | "Etc/GMT-8" | "Etc/GMT-9" | "Etc/UTC" | "Europe/Amsterdam" | "Europe/Andorra" | "Europe/Astrakhan" | "Europe/Athens" | "Europe/Belgrade" | "Europe/Berlin" | "Europe/Bratislava" | "Europe/Brussels" | "Europe/Bucharest" | "Europe/Budapest" | "Europe/Busingen" | "Europe/Chisinau" | "Europe/Copenhagen" | "Europe/Dublin" | "Europe/Gibraltar" | "Europe/Guernsey" | "Europe/Helsinki" | "Europe/Isle_of_Man" | "Europe/Istanbul" | "Europe/Jersey" | "Europe/Kaliningrad" | "Europe/Kiev" | "Europe/Kirov" | "Europe/Kyiv" | "Europe/Lisbon" | "Europe/Ljubljana" | "Europe/London" | "Europe/Luxembourg" | "Europe/Madrid" | "Europe/Malta" | "Europe/Mariehamn" | "Europe/Minsk" | "Europe/Monaco" | "Europe/Moscow" | "Europe/Oslo" | "Europe/Paris" | "Europe/Podgorica" | "Europe/Prague" | "Europe/Riga" | "Europe/Rome" | "Europe/Samara" | "Europe/San_Marino" | "Europe/Sarajevo" | "Europe/Saratov" | "Europe/Simferopol" | "Europe/Skopje" | "Europe/Sofia" | "Europe/Stockholm" | "Europe/Tallinn" | "Europe/Tirane" | "Europe/Ulyanovsk" | "Europe/Uzhgorod" | "Europe/Vaduz" | "Europe/Vatican" | "Europe/Vienna" | "Europe/Vilnius" | "Europe/Volgograd" | "Europe/Warsaw" | "Europe/Zagreb" | "Europe/Zaporozhye" | "Europe/Zurich" | "Indian/Antananarivo" | "Indian/Chagos" | "Indian/Christmas" | "Indian/Cocos" | "Indian/Comoro" | "Indian/Kerguelen" | "Indian/Mahe" | "Indian/Maldives" | "Indian/Mauritius" | "Indian/Mayotte" | "Indian/Reunion" | "Pacific/Apia" | "Pacific/Auckland" | "Pacific/Bougainville" | "Pacific/Chatham" | "Pacific/Chuuk" | "Pacific/Easter" | "Pacific/Efate" | "Pacific/Enderbury" | "Pacific/Fakaofo" | "Pacific/Fiji" | "Pacific/Funafuti" | "Pacific/Galapagos" | "Pacific/Gambier" | "Pacific/Guadalcanal" | "Pacific/Guam" | "Pacific/Honolulu" | "Pacific/Johnston" | "Pacific/Kanton" | "Pacific/Kiritimati" | "Pacific/Kosrae" | "Pacific/Kwajalein" | "Pacific/Majuro" | "Pacific/Marquesas" | "Pacific/Midway" | "Pacific/Nauru" | "Pacific/Niue" | "Pacific/Norfolk" | "Pacific/Noumea" | "Pacific/Pago_Pago" | "Pacific/Palau" | "Pacific/Pitcairn" | "Pacific/Pohnpei" | "Pacific/Ponape" | "Pacific/Port_Moresby" | "Pacific/Rarotonga" | "Pacific/Saipan" | "Pacific/Tahiti" | "Pacific/Tarawa" | "Pacific/Tongatapu" | "Pacific/Truk" | "Pacific/Wake" | "Pacific/Wallis";
@@ -1,17 +1,9 @@
1
1
  import { type ClientChannel } from "ssh2";
2
2
  import type { Console, Device } from "../schema";
3
- /** An open console: the stream to the device's CLI, and how to let it go. */
3
+ /** An open console: the stream to the device's CLI, and how to let it go. `close` waits for the console to go quiet, then lets the port go. */
4
4
  export type ConsoleSession = {
5
5
  channel: ClientChannel;
6
- close: () => void;
7
- };
8
- /** Open a device's serial console, through its jump host or on this computer. Fails when another session holds the port. */
9
- export declare function openConsole(console: Console): Promise<ConsoleSession>;
10
- /** A device's console and its login, for `--console`. */
11
- export declare function consoleOf(device: Device): {
12
- console: Console;
13
- user: string;
14
- password: string;
6
+ close: () => Promise<void>;
15
7
  };
16
8
  /** How a platform's console asks for a login and shows its prompt, and what moves it on from anywhere else. */
17
9
  export type ConsoleDialect = {
@@ -35,25 +27,89 @@ type Channel = Pick<ClientChannel, "write"> & {
35
27
  on(event: "data", listener: (chunk: Buffer) => void): unknown;
36
28
  removeListener(event: "data", listener: (chunk: Buffer) => void): unknown;
37
29
  };
38
- /** Write, then wait until what the console printed since ends with one of the patterns. Returns which, and what was printed. */
39
- export declare function expect(channel: Channel, write: string, patterns: readonly RegExp[], timeout?: number): Promise<{
40
- index: number;
41
- text: string;
42
- }>;
43
- /**
44
- * Log in on a console, whatever state the last session left it in: what `others` names is moved on, a session left open is logged out, so every session starts from a fresh login. Returns once the CLI's prompt shows. A console that asks only for a password (`passwordOnly`) is logged in at its password prompt, with no name typed.
45
- */
46
- export declare function consoleLogin(channel: Channel, dialect: ConsoleDialect, user: string, password: string, passwordOnly?: boolean): Promise<void>;
47
- /** The text after a command's echo. A terminal wraps a long echo where it likes, so the echo is matched character by character, line breaks it added skipped. */
48
- export declare function afterEcho(text: string, command: string): string;
49
- /** A CLI on a console that runs one command at a time and returns what it printed, as an SSH exec would: no echo, no prompt. */
30
+ export declare class SerialConsole {
31
+ private static readonly RELAY_PATH;
32
+ /** A serial port the relay is started with: a path under /dev, which reaches a shell, or a Windows COM port. */
33
+ private static readonly PORT;
34
+ private static readonly WINDOWS_PORT;
35
+ /** Terminal escape sequences: cursor moves, screen clears, character sets and modes. */
36
+ private static readonly ESCAPES;
37
+ /** A session to a jump host, through `cloudflared access ssh` when it sits behind a Cloudflare Tunnel. */
38
+ private static connectJump;
39
+ /**
40
+ * Open a device's serial console, through its jump host or on this computer.
41
+ *
42
+ * A port another program holds is tried again every five seconds, `attempts` times in all, then fails with a message that starts with `busy:`.
43
+ */
44
+ static open(console: Console, attempts?: number): Promise<ConsoleSession>;
45
+ private static openOnJump;
46
+ /** The relay as a child process: Python on Linux and macOS, PowerShell on Windows. */
47
+ private static openHere;
48
+ /** Wait until the relay says it holds the port, or why it cannot. */
49
+ private static relayReady;
50
+ /** A terminal ends a line with a carriage return. Adapters write lines as they do over SSH, and the console gets what a keyboard would send. */
51
+ private static asTerminal;
52
+ /** A device's console and its login, for `--console`. */
53
+ static of(device: Device): {
54
+ console: Console;
55
+ user: string;
56
+ password: string;
57
+ };
58
+ /**
59
+ * Write, then wait until what the console printed since ends with one of the patterns. Returns which, and what was printed.
60
+ *
61
+ * With `settle`, a match counts only once nothing more arrives for that many milliseconds, so a line in the middle of the output that looks like a prompt does not end it.
62
+ */
63
+ static expect(channel: Channel, write: string, patterns: readonly RegExp[], timeout?: number, settle?: number): Promise<{
64
+ index: number;
65
+ text: string;
66
+ }>;
67
+ /**
68
+ * Log in on a console, whatever state the last session left it in: what `others` names is moved on, a session left open is logged out, so every session starts from a fresh login. Returns once the CLI's prompt shows. A console that asks only for a password (`passwordOnly`) is logged in at its password prompt, with no name typed.
69
+ */
70
+ static login(channel: Channel, dialect: ConsoleDialect, user: string, password: string, passwordOnly?: boolean): Promise<void>;
71
+ /** Wait until the console has printed nothing for `silence` milliseconds, dropping what it printed, at most five seconds. */
72
+ private static quiet;
73
+ /** Write, then collect what the console prints until it is quiet for `silence` milliseconds, or for `limit` in all. Carriage returns and escape sequences are dropped. */
74
+ static listen(channel: Channel, write: string, silence: number, limit?: number): Promise<string>;
75
+ /** The text after a command's echo. A terminal wraps a long echo where it likes, so the echo is matched character by character, line breaks it added skipped. */
76
+ static afterEcho(text: string, command: string): string;
77
+ /** Where the echo of a command that starts at `start` ends, or undefined when the text there is not its echo. */
78
+ private static echoEnd;
79
+ }
80
+ /** A CLI on a console or in an SSH shell that runs one command at a time and returns what it printed, as an SSH exec would: no echo, no prompt. */
50
81
  export declare class ConsoleShell {
51
82
  private readonly channel;
52
83
  private readonly dialect;
53
- constructor(channel: Channel, dialect: ConsoleDialect);
84
+ /** Milliseconds of quiet after the prompt shows before a command counts as done. */
85
+ private readonly settle;
86
+ /** The quiet to wait for after the prompt when output can hold a line that looks like it, as `circuit show` prints whatever the device has. */
87
+ static readonly SETTLE = 1000;
88
+ constructor(channel: Channel, dialect: ConsoleDialect,
89
+ /** Milliseconds of quiet after the prompt shows before a command counts as done. */
90
+ settle?: number);
54
91
  /** Run a command and wait for the prompt. A pager in `others` is answered as it shows. */
55
92
  run(command: string, timeout?: number): Promise<string>;
56
93
  /** Log out, so no session is left open on the console. */
57
94
  logout(): void;
58
95
  }
96
+ /** One line for `circuit console`: what to send, the quiet that ends its answer, and the text the last answer must end with for the line to be sent. */
97
+ export type ScriptLine = {
98
+ text: string;
99
+ quiet: number;
100
+ onlyAfter?: string;
101
+ };
102
+ /**
103
+ * Lines sent to a console one at a time, each answer read until the console goes quiet, as a person would type them.
104
+ *
105
+ * A line may start with `@if=TEXT=>`: it is sent only when the last answer ends with TEXT, for a prompt that may or may not come. Then, or alone, `@wait=N:` waits for N seconds of quiet instead of the usual 1.5, for a slow command.
106
+ */
107
+ export declare class ConsoleScript {
108
+ /** The quiet that ends an answer, in milliseconds. */
109
+ static readonly QUIET = 1500;
110
+ /** A line as written, with `quiet` for a line that has no `@wait=`. */
111
+ static parse(line: string, quiet?: number): ScriptLine;
112
+ /** Send each line and hand each answer to `answered` as it comes. A line whose condition does not hold is skipped. */
113
+ static run(channel: Channel, lines: readonly string[], answered: (answer: string) => void, quiet?: number): Promise<void>;
114
+ }
59
115
  export {};
@@ -0,0 +1,32 @@
1
+ /** What the pings found: how many were sent, and the round trip of each that came back, in milliseconds. */
2
+ export type PathMeasure = {
3
+ sent: number;
4
+ times: number[];
5
+ };
6
+ /** Whether an apply may go ahead over the path, and a line that says why. */
7
+ export type PathVerdict = {
8
+ refuse: boolean;
9
+ summary: string;
10
+ };
11
+ export declare class PathCheck {
12
+ /** Pings sent in all. With 20, one lost is 5%. */
13
+ static readonly PINGS = 20;
14
+ /** Ping programs run side by side, so the check takes about five seconds on every system: each sends one ping a second. */
15
+ private static readonly RUNS;
16
+ /** The most loss allowed, in percent: one of 20 pings may be lost, two may not. */
17
+ static readonly MAX_LOSS_PERCENT = 5;
18
+ /** The highest average round trip allowed, in milliseconds. */
19
+ static readonly MAX_AVERAGE_MS = 300;
20
+ /** How long one ping program may run before it is stopped and what it printed is counted. */
21
+ private static readonly LIMIT_MS;
22
+ /**
23
+ * The round trip of every reply in what `ping` printed, in milliseconds. A reply is the one line with a time after `=` or `<` and before `ms`, as in `time=12 ms` or `time<1ms`: the same in every language `ping` prints in. A summary line has several times, or none right before `ms`, and is not counted.
24
+ */
25
+ static parse(output: string): number[];
26
+ /** Whether the path is good enough to apply over. A device that answers no ping at all may only block it, so that alone is not refused. */
27
+ static verdict(host: string, measure: PathMeasure): PathVerdict;
28
+ /** Ping a host with the system's `ping`. Undefined when there is no `ping` to run. */
29
+ static measure(host: string): Promise<PathMeasure | undefined>;
30
+ /** What one run of `ping` printed. Windows takes `-n` and a wait per ping in milliseconds; Linux and macOS take `-c`, and macOS pings IPv6 with `ping6`. */
31
+ private static ping;
32
+ }
@@ -8,18 +8,20 @@ export type Login = {
8
8
  /** Host key algorithms to accept, for a device that negotiates one it then signs wrongly. */
9
9
  hostKeys?: string[];
10
10
  };
11
- /** The address SSH reaches a device at. A device without one is reached only through its console. */
12
- export declare function managementHost(device: Device): string;
13
- /** Open a session. */
14
- export declare function connect(login: Login): Promise<Client>;
15
- /** Run one command and return what it printed, stderr included. */
16
- export declare function exec(client: Client, command: string, timeout?: number): Promise<string>;
17
- /** An interactive shell, for platforms that take configuration only there. */
18
- export declare function shell(client: Client, columns?: number, rows?: number): Promise<ClientChannel>;
19
- /** Collect output until `done` holds, then wait `settle` for stragglers, or give up at `timeout`. */
20
- export declare function readUntil(channel: ClientChannel, done: (output: string) => boolean, timeout: number, settle?: number): Promise<string>;
21
- /** Send one line into an interactive shell and collect output until it goes quiet for `settle`. */
22
- export declare function send(channel: ClientChannel, line: string, timeout?: number, settle?: number): Promise<string>;
23
- export declare const sleep: (milliseconds: number) => Promise<unknown>;
24
- /** Write a file to the device over SFTP. */
25
- export declare function upload(client: Client, path: string, content: string | Buffer): Promise<void>;
11
+ export declare class Ssh {
12
+ /** The address SSH reaches a device at. A device without one is reached only through its console. */
13
+ static hostOf(device: Device): string;
14
+ /** Open a session. */
15
+ static connect(login: Login): Promise<Client>;
16
+ /** Run one command and return what it printed, stderr included. */
17
+ static exec(client: Client, command: string, timeout?: number): Promise<string>;
18
+ /** An interactive shell, for platforms that take configuration only there. */
19
+ static shell(client: Client, columns?: number, rows?: number): Promise<ClientChannel>;
20
+ /** Collect output until `done` holds, then wait `settle` for stragglers, or give up at `timeout`. */
21
+ static readUntil(channel: ClientChannel, done: (output: string) => boolean, timeout: number, settle?: number): Promise<string>;
22
+ /** Send one line into an interactive shell and collect output until it goes quiet for `settle`. */
23
+ static send(channel: ClientChannel, line: string, timeout?: number, settle?: number): Promise<string>;
24
+ static sleep(milliseconds: number): Promise<unknown>;
25
+ /** Write a file to the device over SFTP. */
26
+ static upload(client: Client, path: string, content: string | Buffer): Promise<void>;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -54,10 +54,10 @@
54
54
  "bun": "./src/index.ts",
55
55
  "default": "./dist/index.js"
56
56
  },
57
- "./presets": {
58
- "types": "./dist/types/presets/index.d.ts",
59
- "bun": "./src/presets/index.ts",
60
- "default": "./dist/presets/index.js"
57
+ "./helpers": {
58
+ "types": "./dist/types/helpers/index.d.ts",
59
+ "bun": "./src/helpers/index.ts",
60
+ "default": "./dist/helpers/index.js"
61
61
  }
62
62
  },
63
63
  "bin": {
@@ -88,22 +88,28 @@
88
88
  "docs:preview": "vitepress preview docs",
89
89
  "release": "bun scripts/release.ts",
90
90
  "prepare": "husky || true",
91
- "prepack": "bun run build"
91
+ "prepack": "bun run build",
92
+ "check": "bun run typecheck && bun run test && bun run format:check && bun run docs:build && bun run build && node dist/cli/index.js validate --config templates/single-site/circuit.config.ts && node dist/cli/index.js validate --config templates/edge-router/circuit.config.ts && node dist/cli/index.js validate --config templates/colocation/circuit.config.ts",
93
+ "agents:setup": "npx -y skills experimental_install"
92
94
  },
93
95
  "dependencies": {
96
+ "@clack/prompts": "^1.8.1",
97
+ "@commander-js/extra-typings": "~14.0.0",
94
98
  "@types/ssh2": "^1.15.6",
99
+ "commander": "~14.0.3",
95
100
  "jiti": "^2.7.0",
96
- "ssh2": "^1.17.0"
101
+ "ssh2": "^1.17.0",
102
+ "yaml": "^2.9.1"
97
103
  },
98
104
  "devDependencies": {
99
- "@commitlint/cli": "^19.8.1",
100
- "@commitlint/config-conventional": "^19.8.1",
101
- "@types/bun": "^1.4.2",
105
+ "@commitlint/cli": "^21.2.4",
106
+ "@commitlint/config-conventional": "^21.2.4",
107
+ "@types/bun": "^1.4.4",
102
108
  "@types/node": "^24.19.2",
103
109
  "husky": "^9.1.7",
104
110
  "prettier": "^3.9.10",
105
111
  "typescript": "^7.0.2",
106
112
  "vitepress": "^1.6.4"
107
113
  },
108
- "packageManager": "bun@1.4.3"
114
+ "packageManager": "bun@1.4.4"
109
115
  }
@@ -0,0 +1,91 @@
1
+ // What `circuit graph` prints: the graph of the network with what `validate` found and how each device compares with its last snapshot, as Mermaid, DOT or JSON.
2
+
3
+ import { Topology, type Annotated, type GraphNode } from "../core/topology";
4
+
5
+ export class GraphFormat {
6
+ /** `error` when validate found an error on the device, `warning` for a warning or a difference from its snapshot. */
7
+ static mark(graph: Annotated, node: GraphNode): "error" | "warning" | undefined {
8
+ if (node.kind !== "device") return undefined;
9
+
10
+ const findings = graph.findings.filter((finding) => finding.device && Topology.deviceId(finding.device) === node.id);
11
+ if (findings.some((finding) => finding.level === "error")) return "error";
12
+
13
+ const drift = graph.drift[node.label]?.status;
14
+ if (findings.length || drift === "differs" || drift === "cannot plan") return "warning";
15
+
16
+ return undefined;
17
+ }
18
+
19
+ /** Mermaid, which GitHub draws wherever Markdown is shown. */
20
+ static mermaid(graph: Annotated): string {
21
+ const ids = new Map(graph.nodes.map((node, index) => [node.id, `n${index}`]));
22
+ // Lines of text, each escaped, as one quoted label.
23
+ const text = (...lines: string[]) =>
24
+ `"${lines.map((line) => line.replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;")).join("<br/>")}"`;
25
+
26
+ const shapes: Record<GraphNode["kind"], (label: string) => string> = {
27
+ device: (label) => `[${label}]`,
28
+ segment: (label) => `([${label}])`,
29
+ outside: (label) => `{{${label}}}`,
30
+ };
31
+
32
+ const lines = ["graph LR"];
33
+
34
+ for (const node of graph.nodes) {
35
+ const label = node.kind === "device" ? text(node.label, node.detail[0] ?? "") : text(node.label);
36
+ lines.push(` ${ids.get(node.id)}${shapes[node.kind](label)}`);
37
+ }
38
+
39
+ for (const edge of graph.edges) {
40
+ const arrow = edge.oneSided ? "-.-" : "---";
41
+ lines.push(` ${ids.get(edge.from)} ${arrow}|${text(edge.label)}| ${ids.get(edge.to)}`);
42
+ }
43
+
44
+ lines.push(
45
+ " classDef error stroke:#d1242f,stroke-width:3px",
46
+ " classDef warning stroke:#bf8700,stroke-width:3px",
47
+ " classDef outside stroke-dasharray:4 3"
48
+ );
49
+
50
+ for (const node of graph.nodes) {
51
+ const mark = GraphFormat.mark(graph, node) ?? (node.kind === "outside" ? "outside" : undefined);
52
+ if (mark) lines.push(` class ${ids.get(node.id)} ${mark}`);
53
+ }
54
+
55
+ return lines.join("\n") + "\n";
56
+ }
57
+
58
+ /** DOT, which Graphviz turns into an image: `circuit graph --format=dot | dot -Tsvg > network.svg`. */
59
+ static dot(graph: Annotated): string {
60
+ const text = (value: string) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
61
+ const colors = { error: "#d1242f", warning: "#bf8700" };
62
+
63
+ const shapes: Record<GraphNode["kind"], string> = {
64
+ device: "shape=box, style=rounded",
65
+ segment: "shape=ellipse",
66
+ outside: "shape=box, style=dashed",
67
+ };
68
+
69
+ const lines = [
70
+ "graph network {",
71
+ " rankdir=LR;",
72
+ ' node [fontname="Helvetica"];',
73
+ ' edge [fontname="Helvetica", fontsize=10];',
74
+ ];
75
+
76
+ for (const node of graph.nodes) {
77
+ const label = node.kind === "device" ? `${node.label}\n${node.detail[0] ?? ""}` : node.label;
78
+ const mark = GraphFormat.mark(graph, node);
79
+ const color = mark ? `, color=${text(colors[mark])}, penwidth=2` : "";
80
+ lines.push(` ${text(node.id)} [label=${text(label)}, ${shapes[node.kind]}${color}];`);
81
+ }
82
+
83
+ for (const edge of graph.edges) {
84
+ const style = edge.oneSided ? ", style=dashed" : "";
85
+ lines.push(` ${text(edge.from)} -- ${text(edge.to)} [label=${text(edge.label)}${style}];`);
86
+ }
87
+
88
+ lines.push("}");
89
+ return lines.join("\n") + "\n";
90
+ }
91
+ }