@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
@@ -0,0 +1,342 @@
1
+ // Reading an export into commands, and normalising both sides so that only a real difference survives a comparison.
2
+
3
+ import { Address } from "../../core/address";
4
+
5
+ export type Command = {
6
+ /** The menu the command belongs to, such as `/ip address`. */
7
+ menu: string;
8
+ verb: "add" | "set";
9
+ /** What a `set` addresses: `[ find default-name=ether1 ]`, `ftp`, `1`, or nothing for a menu of one. */
10
+ selector: string;
11
+ /** Words without a value, such as `blackhole`. */
12
+ flags: string[];
13
+ /** Properties, unquoted and unescaped. */
14
+ attrs: Record<string, string>;
15
+ };
16
+
17
+ export type Parsed = {
18
+ commands: Command[];
19
+ /** Address lists whose entries are filled by `refresh`, not by the render. */
20
+ generated: Set<string>;
21
+ };
22
+
23
+ export class RouterOsExport {
24
+ /**
25
+ * Values the platform holds when nothing was set, per menu, and under `*` for every menu.
26
+ *
27
+ * A property at its default is dropped from both sides of a comparison, and this is the value a property the config does not state is reset to. A property the device holds that is not here cannot be reset, which the plan reports as a problem.
28
+ */
29
+ private static readonly DEFAULTS: Record<string, Record<string, string>> = {
30
+ "*": { disabled: "no", comment: "", instance: "default", "routing-table": "main", vrf: "main" },
31
+ "/interface bridge": { "protocol-mode": "rstp", priority: "0x8000", "vlan-filtering": "no", pvid: "1" },
32
+ "/interface bridge port": { pvid: "1", "frame-types": "admit-all", edge: "auto", "path-cost": "10", priority: "0x80" },
33
+ "/interface bonding": { mode: "balance-rr", "transmit-hash-policy": "layer-2", mtu: "1500" },
34
+ "/interface ethernet": { mtu: "1500", "auto-negotiation": "yes" },
35
+ "/interface ethernet switch port": {
36
+ "storm-rate": "100",
37
+ "limit-broadcasts": "yes",
38
+ "limit-unknown-multicasts": "no",
39
+ "limit-unknown-unicasts": "no",
40
+ },
41
+ "/interface vlan": { "l3-hw-offloading": "yes", mtu: "1500" },
42
+ "/interface macvlan": { mode: "bridge", mtu: "1500" },
43
+ "/interface vrrp": { priority: "100", "preemption-mode": "yes", interval: "1s", version: "3", "v3-protocol": "ipv4" },
44
+ "/interface vxlan": { port: "4789" },
45
+ "/interface wireguard peers": {
46
+ "client-allowed-address": "",
47
+ "endpoint-address": "",
48
+ "endpoint-port": "0",
49
+ "persistent-keepalive": "0s",
50
+ },
51
+ "/ipv6 address": { advertise: "yes" },
52
+ "/routing bgp connection": { "use-bfd": "no" },
53
+ // A timer never set prints nothing, and a session then runs at these. Set to them, it prints them.
54
+ "/routing bfd configuration": { "min-rx": "200ms", "min-tx": "200ms", multiplier: "5" },
55
+ "/ip dhcp-server": { "lease-time": "30m" },
56
+ "/ip dns": { servers: "", "allow-remote-requests": "no" },
57
+ "/ip service": { address: "", certificate: "none" },
58
+ "/ip ssh": { "strong-crypto": "no" },
59
+ "/ip traffic-flow": { enabled: "no", interfaces: "all", "packet-sampling": "no" },
60
+ "/tool mac-server": { "allowed-interface-list": "all" },
61
+ "/tool mac-server mac-winbox": { "allowed-interface-list": "all" },
62
+ "/tool mac-server ping": { enabled: "yes" },
63
+ "/tool bandwidth-server": { enabled: "yes" },
64
+ "/ip neighbor discovery-settings": { "discover-interface-list": "!dynamic", protocol: "cdp,lldp,mndp" },
65
+ "/snmp": { enabled: "no", contact: "", location: "" },
66
+ "/snmp community": {
67
+ addresses: "::/0",
68
+ name: "public",
69
+ security: "none",
70
+ "authentication-protocol": "MD5",
71
+ "encryption-protocol": "DES",
72
+ "read-access": "yes",
73
+ "write-access": "no",
74
+ },
75
+ "/ip settings": { "send-redirects": "yes", "tcp-syncookies": "no" },
76
+ "/system clock": { "time-zone-autodetect": "yes", "time-zone-name": "manual" },
77
+ "/system note": { note: "", "show-at-login": "yes" },
78
+ "/system ntp client": { enabled: "no" },
79
+ "/system ntp server": { enabled: "no" },
80
+ "/system logging action": { "disk-file-count": "2", "disk-lines-per-file": "1000" },
81
+ "/system package update": { channel: "stable" },
82
+ "/system routerboard settings": { "enter-setup-on": "any-key" },
83
+ "/interface ethernet switch": { "l3-hw-offloading": "no" },
84
+ "/interface ethernet switch l3hw-settings": { autorestart: "no", "ipv6-hw": "no" },
85
+ };
86
+
87
+ /** Whether each `/ip service` ships disabled. */
88
+ private static readonly SERVICE_DISABLED: Record<string, string> = {
89
+ ftp: "no",
90
+ ssh: "no",
91
+ telnet: "no",
92
+ www: "no",
93
+ "www-ssl": "yes",
94
+ "reverse-proxy": "no",
95
+ winbox: "no",
96
+ api: "no",
97
+ "api-ssl": "no",
98
+ };
99
+
100
+ /** Helpers of `/ip firewall service-port` that ship disabled. The rest ship enabled. */
101
+ private static readonly HELPER_DISABLED: Record<string, string> = { irc: "yes", rtsp: "yes" };
102
+
103
+ /** A property the device stops printing once another returns to its default. Resetting that other one clears it, so it is never reset on its own. */
104
+ static readonly CLEARED_WITH: Record<string, Record<string, string>> = {
105
+ "/interface ethernet": { speed: "auto-negotiation" },
106
+ };
107
+
108
+ /** Properties whose value cannot be read back from the device. They are compared only by the presence of what holds them. */
109
+ static readonly SECRET_KEYS = new Set(["private-key", "password", "tcp-md5-key", "authentication-password", "encryption-password"]);
110
+
111
+ /** Properties that tell two `add` entries of a menu apart. `name` where the menu is not listed. */
112
+ private static readonly IDENTITY: Record<string, string[]> = {
113
+ "/interface list member": ["list", "interface"],
114
+ "/ip firewall address-list": ["list", "address"],
115
+ "/ipv6 firewall address-list": ["list", "address"],
116
+ "/interface bridge port": ["interface"],
117
+ "/interface bridge vlan": ["vlan-ids"],
118
+ // One VLAN ID per interface: the device refuses a second, so a rename, such as one a MAC of its own brings, is a rename.
119
+ "/interface vlan": ["interface", "vlan-id"],
120
+ "/interface vxlan vteps": ["interface", "remote-ip"],
121
+ "/routing bgp connection": ["remote.address"],
122
+ "/routing rpki": ["address"],
123
+ "/routing ospf interface-template": ["area", "interfaces"],
124
+ "/routing bfd configuration": ["interfaces"],
125
+ "/ip route": ["dst-address", "routing-table"],
126
+ "/ipv6 route": ["dst-address", "routing-table"],
127
+ "/ip address": ["address"],
128
+ "/ipv6 address": ["address"],
129
+ "/ip dhcp-server network": ["address"],
130
+ "/ip dhcp-server lease": ["address"],
131
+ "/ip traffic-flow target": ["dst-address", "port"],
132
+ "/system ntp client servers": ["address"],
133
+ "/system logging": ["action", "topics"],
134
+ "/user ssh-keys": ["user", "key"],
135
+ };
136
+
137
+ /** Menus whose entries are an ordered list without names. They are rewritten whole, in the order the render gives. */
138
+ static readonly ORDERED = new Set([
139
+ "/routing filter rule",
140
+ "/ip firewall filter",
141
+ "/ipv6 firewall filter",
142
+ "/ip firewall nat",
143
+ "/ipv6 firewall nat",
144
+ "/ip firewall mangle",
145
+ "/ipv6 firewall mangle",
146
+ "/ip firewall raw",
147
+ "/ipv6 firewall raw",
148
+ "/interface bridge filter",
149
+ "/interface ethernet switch rule",
150
+ ]);
151
+
152
+ /** Undo the line wrapping an export applies: a backslash at the end of a line continues it. */
153
+ private static unwrap(text: string): string {
154
+ return text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
155
+ }
156
+
157
+ /** A quoted value without its quotes and escapes. `\_` is an escaped space. */
158
+ private static unquote(value: string): string {
159
+ if (!value.startsWith('"')) return value;
160
+
161
+ return value.slice(1, -1).replace(/\\(.)/g, (_, escaped: string) => (escaped === "_" ? " " : escaped));
162
+ }
163
+
164
+ /** Split a command into words, keeping quoted values and bracketed selectors whole. */
165
+ private static words(line: string): string[] {
166
+ const result: string[] = [];
167
+ let current = "";
168
+ let inQuotes = false;
169
+ let bracketDepth = 0;
170
+
171
+ for (let i = 0; i < line.length; i++) {
172
+ const char = line[i]!;
173
+ if (inQuotes) {
174
+ current += char;
175
+ if (char === "\\") current += line[++i] ?? "";
176
+ else if (char === '"') inQuotes = false;
177
+ continue;
178
+ }
179
+
180
+ if (char === '"') inQuotes = true;
181
+ if (char === "[") bracketDepth++;
182
+ if (char === "]") bracketDepth--;
183
+ if (char === " " && bracketDepth === 0) {
184
+ if (current) result.push(current);
185
+ current = "";
186
+ continue;
187
+ }
188
+
189
+ current += char;
190
+ }
191
+
192
+ if (current) result.push(current);
193
+ return result;
194
+ }
195
+
196
+ /** One `add` or `set` line into a command. */
197
+ private static parseLine(menu: string, line: string): Command {
198
+ const [verb, ...rest] = RouterOsExport.words(line) as ["add" | "set", ...string[]];
199
+
200
+ let selector = "";
201
+ const first = rest[0];
202
+ if (verb === "set" && first && (first.startsWith("[") || !first.includes("="))) {
203
+ selector = rest.shift()!.replace(/\s+/g, " ");
204
+ }
205
+
206
+ const flags: string[] = [];
207
+ const attrs: Record<string, string> = {};
208
+ // A property of the same group as the one before it is written `.name`, which only reads back inside its own command, so it is expanded here.
209
+ let group = "";
210
+
211
+ for (const word of rest) {
212
+ const equals = word.indexOf("=");
213
+ if (equals < 0) {
214
+ flags.push(word);
215
+ continue;
216
+ }
217
+
218
+ let name = word.slice(0, equals);
219
+ if (name.startsWith(".")) name = group + name;
220
+ else if (name.includes(".")) group = name.split(".")[0]!;
221
+ attrs[name] = RouterOsExport.unquote(word.slice(equals + 1));
222
+ }
223
+
224
+ return { menu, verb, selector, flags, attrs };
225
+ }
226
+
227
+ static parse(text: string): Parsed {
228
+ const commands: Command[] = [];
229
+ const generated = new Set<string>();
230
+ let menu = "";
231
+
232
+ for (const raw of RouterOsExport.unwrap(text).split("\n")) {
233
+ const line = raw.trim();
234
+ if (line.startsWith("/")) {
235
+ menu = line;
236
+ continue;
237
+ }
238
+
239
+ const marker = /^# generated (\S+)$/.exec(line);
240
+ if (marker) generated.add(marker[1]!);
241
+ if (menu && /^(add|set)( |$)/.test(line)) commands.push(RouterOsExport.parseLine(menu, line));
242
+ }
243
+
244
+ return { commands, generated };
245
+ }
246
+
247
+ /** The default of one property of a command, or undefined when it is not known. */
248
+ static defaultOf(command: Command, property: string): string | undefined {
249
+ if (command.menu === "/ip service" && property === "disabled") return RouterOsExport.SERVICE_DISABLED[command.selector];
250
+
251
+ if (command.menu === "/ip firewall service-port" && property === "disabled") {
252
+ return RouterOsExport.HELPER_DISABLED[command.selector] ?? "no";
253
+ }
254
+
255
+ return RouterOsExport.DEFAULTS[command.menu]?.[property] ?? RouterOsExport.DEFAULTS["*"]![property];
256
+ }
257
+
258
+ /** How the renderer writes a secret. */
259
+ static isSecretMarker(value: string): boolean {
260
+ return value.startsWith("<secret:");
261
+ }
262
+
263
+ /** A filter rule as the platform prints it, with its own spacing, squeezed so that only a real change survives. */
264
+ private static canonicalRule(rule: string): string {
265
+ return rule
266
+ .replace(/\s*([=&|;{}(),<>!]+)\s*/g, "$1")
267
+ .replace(/\s+/g, " ")
268
+ .trim();
269
+ }
270
+
271
+ /** One property value in the form both sides compare in. */
272
+ private static normalValue(command: Command, property: string, value: string): string {
273
+ if (property === "rule") return RouterOsExport.canonicalRule(value);
274
+ // A list whose order means nothing.
275
+ if (property === "tagged" || property === "untagged" || property === "slaves") return value.split(",").sort().join(",");
276
+
277
+ const onInterface = property === "address" && (command.menu === "/ip address" || command.menu === "/ipv6 address");
278
+ // An IP address property, which a MAC address is not.
279
+ const isAddress = /(^|-)address$/.test(property) && !property.endsWith("mac-address");
280
+ // On an interface, a bare address is shorthand for /32 on IPv4 and /64 on IPv6.
281
+ if (onInterface && !value.includes("/")) return `${value}/${value.includes(":") ? 64 : 32}`;
282
+ // Elsewhere a host is printed bare on IPv4 and with /128 on IPv6.
283
+ if (!onInterface && isAddress && /^[\d.]+\/32$/.test(value)) return value.slice(0, -3);
284
+
285
+ if (!onInterface && isAddress && value.includes(":") && !value.includes("/") && !value.includes(",")) return `${value}/128`;
286
+
287
+ return value;
288
+ }
289
+
290
+ /** A command's properties without defaults or secrets, in the form both sides compare in. */
291
+ static normal(command: Command): Record<string, string> {
292
+ const result: Record<string, string> = {};
293
+
294
+ for (const [property, raw] of Object.entries(command.attrs)) {
295
+ if (RouterOsExport.SECRET_KEYS.has(property) || RouterOsExport.isSecretMarker(raw)) continue;
296
+
297
+ const value = RouterOsExport.normalValue(command, property, raw);
298
+ if (value === RouterOsExport.defaultOf(command, property)) continue;
299
+
300
+ result[property] = value;
301
+ }
302
+ // The platform prints the network it derived from an address. Only a network it could not derive says something.
303
+ if (
304
+ command.menu === "/ip address" &&
305
+ result.network &&
306
+ result.address &&
307
+ Address.prefixOf(result.address).split("/")[0] === result.network
308
+ )
309
+ delete result.network;
310
+ return result;
311
+ }
312
+
313
+ /** What identifies an `add` entry, so that the same entry is found on both sides. */
314
+ static identity(command: Command): Record<string, string> {
315
+ // The switching bridge is the one that filters VLANs, whatever it is called, so a rename is a rename and not a rebuild of every port.
316
+ if (command.menu === "/interface bridge" && command.attrs["vlan-filtering"] === "yes") return { "vlan-filtering": "yes" };
317
+
318
+ // A BFD configuration covers interfaces, or the addresses of multihop sessions, never both. A selector on a property that is not set matches nothing, so only the one that is set identifies it.
319
+ const addresses = command.attrs.addresses;
320
+ if (command.menu === "/routing bfd configuration" && addresses !== undefined) return { addresses };
321
+
322
+ const normalized = RouterOsExport.normal(command);
323
+ const properties = RouterOsExport.IDENTITY[command.menu] ?? ["name"];
324
+ const result: Record<string, string> = {};
325
+
326
+ for (const property of properties) {
327
+ const value = normalized[property] ?? command.attrs[property] ?? RouterOsExport.defaultOf(command, property) ?? "";
328
+ result[property] = value;
329
+ }
330
+
331
+ return result;
332
+ }
333
+
334
+ /** One command as a string that is equal for equal commands. */
335
+ static canonical(command: Command): string {
336
+ const properties = Object.entries(RouterOsExport.normal(command))
337
+ .sort(([a], [b]) => a.localeCompare(b))
338
+ .map(([name, value]) => `${name}=${value}`);
339
+ const flags = [...command.flags].sort();
340
+ return [command.verb, command.selector, ...flags, ...properties].filter(Boolean).join(" ");
341
+ }
342
+ }