@takodotid/circuit 0.34.3 → 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 +11345 -9024
  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 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  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
@@ -3,11 +3,11 @@
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { portsOf } from "../adapters/devices/catalog";
7
- import { familyOf, host } from "../core/addr";
8
- import { type AddressReport, addressesOf } from "../core/addresses";
9
- import { type Annotated, type Holder, holdersOf, keyOf } from "../core/graph";
10
- import { SCHEMA_FILES, schemaTypes } from "../core/schema-docs";
6
+ import { Catalog } from "../platforms/catalog";
7
+ import { Address } from "../core/address";
8
+ import { AddressSpace, type AddressReport } from "../core/address-space";
9
+ import { Topology, type Annotated, type Holder } from "../core/topology";
10
+ import { SchemaDocs } from "../core/schema-docs";
11
11
  import type { Device } from "../schema";
12
12
 
13
13
  /** One BGP session, with its group's settings filled in, and what is at its far end. */
@@ -68,188 +68,192 @@ export type StudioData = {
68
68
  docs: Record<string, TypeDocs>;
69
69
  };
70
70
 
71
- function sessionsOf(devices: readonly Device[], owners: Map<string, Holder>): Session[] {
72
- const all = devices.flatMap((device) => {
73
- const bgp = device.routing?.bgp;
74
-
75
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]): Session & { deviceObject: Device } => {
76
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
77
- const settings = { ...group, ...neighbor };
78
-
79
- return {
80
- deviceObject: device,
81
- device: device.name,
82
- name,
83
- address: neighbor.address,
84
- group: neighbor.group,
85
- remoteAs: settings.remote_as,
86
- role: settings.role,
87
- import: settings.import,
88
- export: settings.export,
89
- description: settings.description,
90
- };
91
- });
92
- });
93
-
94
- const ownerOf = (address: string) => owners.get(keyOf(address) ?? "")?.device;
71
+ /** Where a device is kept, relative to the config: the file that declares it, and its snapshot. Either is absent when it cannot be found. */
72
+ export type DeviceFiles = { source?: string; snapshot?: string };
95
73
 
96
- return all.map(({ deviceObject, ...session }) => {
97
- const far = ownerOf(session.address);
74
+ /** How Studio learns what a device would run and what it ran, from the caller, which knows the adapters and the state directory. */
75
+ export type DeviceTexts = {
76
+ /** What the device would run, as `circuit build` prints it. */
77
+ render: (device: Device) => string;
78
+ /** Its last snapshot, or undefined when it has none. */
79
+ snapshot: (device: Device) => string | undefined;
80
+ /** Its file and its snapshot's file. */
81
+ files: (device: Device) => DeviceFiles;
82
+ };
98
83
 
99
- if (far && far !== deviceObject) {
100
- // The session back is one of the same family: a v4 session back does not answer a v6 session.
101
- const back = all.some(
102
- (other) =>
103
- other.deviceObject === far &&
104
- ownerOf(other.address) === deviceObject &&
105
- familyOf(other.address) === familyOf(session.address)
106
- );
107
- return { ...session, far: far.name, ...(back ? {} : { oneSided: true as const }) };
84
+ export class StudioModel {
85
+ /** Everything Studio shows. `graph` comes ready from the caller, with each device compared with its snapshot. */
86
+ static build(devices: readonly Device[], graph: Annotated, texts: DeviceTexts): StudioData {
87
+ const owners = new Map<string, Holder>();
88
+ for (const holder of Topology.holders(devices)) {
89
+ const key = Topology.keyOf(Address.host(holder.address))!;
90
+ if (!owners.has(key)) owners.set(key, holder);
108
91
  }
109
92
 
110
- return { ...session, ...(session.remoteAs ? { far: `AS${session.remoteAs}` } : {}) };
111
- });
112
- }
93
+ const attempt = <T>(work: () => T, otherwise: (error: Error) => T) => {
94
+ try {
95
+ return work();
96
+ } catch (error) {
97
+ return otherwise(error as Error);
98
+ }
99
+ };
113
100
 
114
- function cablesOf(devices: readonly Device[]): Cable[] {
115
- const byName = new Map(devices.map((device) => [device.name, device]));
116
- const drawn = new Set<string>();
117
- const cables: Cable[] = [];
118
-
119
- const vlansOf = (device: Device, portName: string) => {
120
- const port = device.ports?.[portName];
121
- const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
122
- const carrier = lag?.type === "lag" ? lag : port;
123
- const names = [carrier?.access_vlan, carrier?.native_vlan, ...(carrier?.trunk_vlans ?? [])].filter((name) => name !== undefined);
124
-
125
- return [...new Set(names)].map((name) => `${name} (${device.vlans?.[name]?.id ?? "?"})`);
126
- };
127
-
128
- for (const device of devices) {
129
- for (const [portName, port] of Object.entries(device.ports ?? {})) {
130
- const link = port?.link;
131
- if (!port || !link) continue;
132
-
133
- const here = `${device.name} ${portName}`;
134
- const there = `${link.device} ${link.port}`;
135
- if (drawn.has(`${there}|${here}`)) continue;
136
- drawn.add(`${here}|${there}`);
137
-
138
- const farPort = byName.get(link.device)?.ports?.[link.port];
139
- const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
140
-
141
- cables.push({
142
- device: device.name,
143
- port: portName,
144
- farDevice: link.device,
145
- farPort: link.port,
146
- ...(port.lag ? { lag: port.lag } : {}),
147
- ...(farPort?.lag ? { farLag: farPort.lag } : {}),
148
- ...(port.description ? { description: port.description } : {}),
149
- vlans: vlansOf(device, portName),
150
- ...(linksBack ? {} : { oneSided: true as const }),
151
- });
152
- }
101
+ return {
102
+ graph,
103
+ devices,
104
+ ports: Object.fromEntries(
105
+ devices.map((device) => [
106
+ device.name,
107
+ attempt(
108
+ () => Catalog.ports(device.platform, device.model),
109
+ () => []
110
+ ),
111
+ ])
112
+ ),
113
+ rendered: Object.fromEntries(
114
+ devices.map((device) => [
115
+ device.name,
116
+ attempt(
117
+ () => texts.render(device),
118
+ (error) => `Cannot be rendered: ${error.message}`
119
+ ),
120
+ ])
121
+ ),
122
+ snapshots: Object.fromEntries(
123
+ devices.flatMap((device) => {
124
+ const text = texts.snapshot(device);
125
+ return text === undefined ? [] : [[device.name, text]];
126
+ })
127
+ ),
128
+ files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
129
+ addresses: AddressSpace.report(devices),
130
+ sessions: StudioModel.sessionsOf(devices, owners),
131
+ cables: StudioModel.cablesOf(devices),
132
+ docs: StudioModel.schemaDocs(),
133
+ };
153
134
  }
154
135
 
155
- return cables;
156
- }
157
-
158
- /** The directory Circuit is installed in: the nearest one up with its package.json. Its sources ship with it, in src: the schema, and the files of Studio's page. */
159
- export function packageRoot(): string | undefined {
160
- let directory = dirname(fileURLToPath(import.meta.url));
136
+ /** The directory Circuit is installed in: the nearest one up with its package.json. Its sources ship with it, in src: the schema, and the files of Studio's page. */
137
+ static packageRoot(): string | undefined {
138
+ let directory = dirname(fileURLToPath(import.meta.url));
161
139
 
162
- while (true) {
163
- const manifest = join(directory, "package.json");
164
- if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@takodotid/circuit") return directory;
140
+ while (true) {
141
+ const manifest = join(directory, "package.json");
142
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@takodotid/circuit") return directory;
165
143
 
166
- const parent = dirname(directory);
167
- if (parent === directory) return undefined;
168
- directory = parent;
144
+ const parent = dirname(directory);
145
+ if (parent === directory) return undefined;
146
+ directory = parent;
147
+ }
169
148
  }
170
- }
171
149
 
172
- /** The schema's descriptions, or none when its sources cannot be found. */
173
- function schemaDocs(): Record<string, TypeDocs> {
174
- const root = packageRoot();
175
- if (!root) return {};
150
+ private static sessionsOf(devices: readonly Device[], owners: Map<string, Holder>): Session[] {
151
+ const all = devices.flatMap((device) => {
152
+ const bgp = device.routing?.bgp;
153
+
154
+ return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]): Session & { deviceObject: Device } => {
155
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
156
+ const settings = { ...group, ...neighbor };
157
+
158
+ return {
159
+ deviceObject: device,
160
+ device: device.name,
161
+ name,
162
+ address: neighbor.address,
163
+ group: neighbor.group,
164
+ remoteAs: settings.remote_as,
165
+ role: settings.role,
166
+ import: settings.import,
167
+ export: settings.export,
168
+ description: settings.description,
169
+ };
170
+ });
171
+ });
176
172
 
177
- const sources = SCHEMA_FILES.map(([file]) => readFileSync(join(root, "src", "schema", file), "utf8"));
178
- const docs: Record<string, TypeDocs> = {};
173
+ const ownerOf = (address: string) => owners.get(Topology.keyOf(address) ?? "")?.device;
179
174
 
180
- for (const type of schemaTypes(sources).flat()) {
181
- docs[type.name] = {
182
- description: type.description,
183
- includes: (type.includes ?? []).map((name) => name.replace(/<.*$/, "")),
184
- fields: Object.fromEntries(
185
- type.fields.map((field) => [field.path.replace(/\?$/, ""), { type: field.type, description: field.description }])
186
- ),
187
- };
175
+ return all.map(({ deviceObject, ...session }) => {
176
+ const far = ownerOf(session.address);
177
+
178
+ if (far && far !== deviceObject) {
179
+ // The session back is one of the same family: a v4 session back does not answer a v6 session.
180
+ const back = all.some(
181
+ (other) =>
182
+ other.deviceObject === far &&
183
+ ownerOf(other.address) === deviceObject &&
184
+ Address.family(other.address) === Address.family(session.address)
185
+ );
186
+ return { ...session, far: far.name, ...(back ? {} : { oneSided: true as const }) };
187
+ }
188
+
189
+ return { ...session, ...(session.remoteAs ? { far: `AS${session.remoteAs}` } : {}) };
190
+ });
188
191
  }
189
192
 
190
- return docs;
191
- }
193
+ private static cablesOf(devices: readonly Device[]): Cable[] {
194
+ const byName = new Map(devices.map((device) => [device.name, device]));
195
+ const drawn = new Set<string>();
196
+ const cables: Cable[] = [];
197
+
198
+ const vlansOf = (device: Device, portName: string) => {
199
+ const port = device.ports?.[portName];
200
+ const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
201
+ const carrier = lag?.type === "lag" ? lag : port;
202
+ const names = [carrier?.access_vlan, carrier?.native_vlan, ...(carrier?.trunk_vlans ?? [])].filter(
203
+ (name) => name !== undefined
204
+ );
192
205
 
193
- /** Where a device is kept, relative to the config: the file that declares it, and its snapshot. Either is absent when it cannot be found. */
194
- export type DeviceFiles = { source?: string; snapshot?: string };
206
+ return [...new Set(names)].map((name) => `${name} (${device.vlans?.[name]?.id ?? "?"})`);
207
+ };
195
208
 
196
- /** How Studio learns what a device would run and what it ran, from the caller, which knows the adapters and the state directory. */
197
- export type DeviceTexts = {
198
- /** What the device would run, as `circuit build` prints it. */
199
- render: (device: Device) => string;
200
- /** Its last snapshot, or undefined when it has none. */
201
- snapshot: (device: Device) => string | undefined;
202
- /** Its file and its snapshot's file. */
203
- files: (device: Device) => DeviceFiles;
204
- };
209
+ for (const device of devices) {
210
+ for (const [portName, port] of Object.entries(device.ports ?? {})) {
211
+ const link = port?.link;
212
+ if (!port || !link) continue;
213
+
214
+ const here = `${device.name} ${portName}`;
215
+ const there = `${link.device} ${link.port}`;
216
+ if (drawn.has(`${there}|${here}`)) continue;
217
+ drawn.add(`${here}|${there}`);
218
+
219
+ const farPort = byName.get(link.device)?.ports?.[link.port];
220
+ const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
221
+
222
+ cables.push({
223
+ device: device.name,
224
+ port: portName,
225
+ farDevice: link.device,
226
+ farPort: link.port,
227
+ ...(port.lag ? { lag: port.lag } : {}),
228
+ ...(farPort?.lag ? { farLag: farPort.lag } : {}),
229
+ ...(port.description ? { description: port.description } : {}),
230
+ vlans: vlansOf(device, portName),
231
+ ...(linksBack ? {} : { oneSided: true as const }),
232
+ });
233
+ }
234
+ }
205
235
 
206
- /** Everything Studio shows. `graph` comes ready from the caller, with each device compared with its snapshot. */
207
- export function studioData(devices: readonly Device[], graph: Annotated, texts: DeviceTexts): StudioData {
208
- const owners = new Map<string, Holder>();
209
- for (const holder of holdersOf(devices)) {
210
- const key = keyOf(host(holder.address))!;
211
- if (!owners.has(key)) owners.set(key, holder);
236
+ return cables;
212
237
  }
213
238
 
214
- const attempt = <T>(work: () => T, otherwise: (error: Error) => T) => {
215
- try {
216
- return work();
217
- } catch (error) {
218
- return otherwise(error as Error);
219
- }
220
- };
221
-
222
- return {
223
- graph,
224
- devices,
225
- ports: Object.fromEntries(
226
- devices.map((device) => [
227
- device.name,
228
- attempt(
229
- () => portsOf(device.platform, device.model),
230
- () => []
231
- ),
232
- ])
233
- ),
234
- rendered: Object.fromEntries(
235
- devices.map((device) => [
236
- device.name,
237
- attempt(
238
- () => texts.render(device),
239
- (error) => `Cannot be rendered: ${error.message}`
239
+ /** The schema's descriptions, or none when its sources cannot be found. */
240
+ private static schemaDocs(): Record<string, TypeDocs> {
241
+ const root = StudioModel.packageRoot();
242
+ if (!root) return {};
243
+
244
+ const sources = SchemaDocs.FILES.map(([file]) => readFileSync(join(root, "src", "schema", file), "utf8"));
245
+ const docs: Record<string, TypeDocs> = {};
246
+
247
+ for (const type of SchemaDocs.read(sources).flat()) {
248
+ docs[type.name] = {
249
+ description: type.description,
250
+ includes: (type.includes ?? []).map((name) => name.replace(/<.*$/, "")),
251
+ fields: Object.fromEntries(
252
+ type.fields.map((field) => [field.path.replace(/\?$/, ""), { type: field.type, description: field.description }])
240
253
  ),
241
- ])
242
- ),
243
- snapshots: Object.fromEntries(
244
- devices.flatMap((device) => {
245
- const text = texts.snapshot(device);
246
- return text === undefined ? [] : [[device.name, text]];
247
- })
248
- ),
249
- files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
250
- addresses: addressesOf(devices),
251
- sessions: sessionsOf(devices, owners),
252
- cables: cablesOf(devices),
253
- docs: schemaDocs(),
254
- };
254
+ };
255
+ }
256
+
257
+ return docs;
258
+ }
255
259
  }
package/src/studio/git.ts CHANGED
@@ -14,106 +14,111 @@ export type Commit = {
14
14
  subject: string;
15
15
  };
16
16
 
17
- /** The environment without git's own variables, such as GIT_DIR in a hook, which would point git at another repository than the directory's. */
18
- const environment = () => Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_")));
19
-
20
- /** Run git in a directory, and give what it printed, or fail with what it said. */
21
- function git(cwd: string, args: string[]): Promise<string> {
22
- return new Promise((done, fail) => {
23
- execFile("git", args, { cwd, env: environment(), maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
24
- if (error) fail(new Error((stderr || error.message).trim()));
25
- else done(stdout);
26
- });
27
- });
28
- }
17
+ export class Git {
18
+ private static readonly FORMAT = "--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s";
29
19
 
30
- const FORMAT = "--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s";
20
+ /** The environment without git's own variables, such as GIT_DIR in a hook, which would point git at another repository than the directory's. */
21
+ private static environment(): Record<string, string | undefined> {
22
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_")));
23
+ }
31
24
 
32
- function commitsOf(text: string): Commit[] {
33
- return text
34
- .split("\n")
35
- .filter(Boolean)
36
- .map((line) => {
37
- const [commit = "", short = "", author = "", date = "", subject = ""] = line.split("\x1f");
38
- return { commit, short, author, date, subject };
25
+ /** Run git in a directory, and give what it printed, or fail with what it said. */
26
+ private static run(cwd: string, args: string[]): Promise<string> {
27
+ return new Promise((done, fail) => {
28
+ execFile("git", args, { cwd, env: Git.environment(), maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
29
+ if (error) fail(new Error((stderr || error.message).trim()));
30
+ else done(stdout);
31
+ });
39
32
  });
40
- }
33
+ }
41
34
 
42
- /** The top of the git repository the directory is in, or undefined when it is in none, or git is missing. */
43
- export async function gitTop(directory: string): Promise<string | undefined> {
44
- try {
45
- return (await git(directory, ["rev-parse", "--show-toplevel"])).trim();
46
- } catch {
47
- return undefined;
35
+ private static commitsOf(text: string): Commit[] {
36
+ return text
37
+ .split("\n")
38
+ .filter(Boolean)
39
+ .map((line) => {
40
+ const [commit = "", short = "", author = "", date = "", subject = ""] = line.split("\x1f");
41
+ return { commit, short, author, date, subject };
42
+ });
48
43
  }
49
- }
50
44
 
51
- /** The latest commits that changed any of these files, relative to the directory, newest first. */
52
- export async function history(directory: string, files: string[], limit = 50): Promise<Commit[]> {
53
- if (!files.length) return [];
54
- return commitsOf(await git(directory, ["log", `-n${limit}`, FORMAT, "--", ...files]));
55
- }
45
+ /** The top of the git repository the directory is in, or undefined when it is in none, or git is missing. */
46
+ static async top(directory: string): Promise<string | undefined> {
47
+ try {
48
+ return (await Git.run(directory, ["rev-parse", "--show-toplevel"])).trim();
49
+ } catch {
50
+ return undefined;
51
+ }
52
+ }
56
53
 
57
- /** Branches and tags to compare with, and the latest commits. */
58
- export async function references(directory: string): Promise<{ names: string[]; commits: Commit[] }> {
59
- const names = (
60
- await git(directory, [
61
- "for-each-ref",
62
- "--count=100",
63
- "--sort=-committerdate",
64
- "--format=%(refname:short)",
65
- "refs/heads",
66
- "refs/remotes",
67
- "refs/tags",
68
- ])
69
- )
70
- .split("\n")
71
- .filter((name) => name && !name.endsWith("/HEAD"));
72
-
73
- return { names, commits: commitsOf(await git(directory, ["log", "-n30", FORMAT])) };
74
- }
54
+ /** The latest commits that changed any of these files, relative to the directory, newest first. */
55
+ static async history(directory: string, files: string[], limit = 50): Promise<Commit[]> {
56
+ if (!files.length) return [];
57
+ return Git.commitsOf(await Git.run(directory, ["log", `-n${limit}`, Git.FORMAT, "--", ...files]));
58
+ }
75
59
 
76
- /** A branch, tag or commit, as the commit it names, or undefined when it names none. Only the characters a git name can hold are accepted. */
77
- export async function commitOf(directory: string, name: string): Promise<Commit | undefined> {
78
- if (!/^[A-Za-z0-9][\w./@^~-]*$/.test(name)) return undefined;
60
+ /** Branches and tags to compare with, and the latest commits. */
61
+ static async references(directory: string): Promise<{ names: string[]; commits: Commit[] }> {
62
+ const names = (
63
+ await Git.run(directory, [
64
+ "for-each-ref",
65
+ "--count=100",
66
+ "--sort=-committerdate",
67
+ "--format=%(refname:short)",
68
+ "refs/heads",
69
+ "refs/remotes",
70
+ "refs/tags",
71
+ ])
72
+ )
73
+ .split("\n")
74
+ .filter((name) => name && !name.endsWith("/HEAD"));
75
+
76
+ return { names, commits: Git.commitsOf(await Git.run(directory, ["log", "-n30", Git.FORMAT])) };
77
+ }
78
+
79
+ /** A branch, tag or commit, as the commit it names, or undefined when it names none. Only the characters a git name can hold are accepted. */
80
+ static async commitOf(directory: string, name: string): Promise<Commit | undefined> {
81
+ if (!/^[A-Za-z0-9][\w./@^~-]*$/.test(name)) return undefined;
79
82
 
80
- try {
81
- const [commit] = commitsOf(await git(directory, ["log", "-n1", FORMAT, `${name}^{commit}`, "--"]));
82
- return commit;
83
- } catch {
84
- return undefined;
83
+ try {
84
+ const [commit] = Git.commitsOf(await Git.run(directory, ["log", "-n1", Git.FORMAT, `${name}^{commit}`, "--"]));
85
+ return commit;
86
+ } catch {
87
+ return undefined;
88
+ }
85
89
  }
86
- }
87
90
 
88
- /**
89
- * Read the network as it was at a commit: check that commit out beside the project, in `.circuit/cache/`, which git ignores, gather it there with `gather`, then remove the checkout. The config is the file of that name in the same directory, `configDirectory`.
90
- *
91
- * The checkout sits inside the project, so its imports of Circuit resolve to the version installed now.
92
- */
93
- export async function networkAt(
94
- configDirectory: string,
95
- configPath: string,
96
- commit: string,
97
- gather: (configPath: string) => Promise<string>
98
- ): Promise<string> {
99
- const top = await gitTop(configDirectory);
100
- if (!top) throw new Error("the config is not in a git repository");
101
-
102
- const cache = join(configDirectory, ".circuit", "cache");
103
- mkdirSync(cache, { recursive: true });
104
- writeFileSync(join(cache, ".gitignore"), "# Written by Circuit Studio: checkouts of other commits, removed once read.\n*\n");
105
-
106
- // A checkout left behind by a run that stopped is forgotten first.
107
- await git(top, ["worktree", "prune"]);
108
-
109
- const checkout = join(cache, commit.slice(0, 12));
110
- await git(top, ["worktree", "add", "--detach", "--force", checkout, commit]);
111
-
112
- try {
113
- // Where the config sits inside the repository, as git says: comparing paths would trip on two spellings of one directory, such as Windows' short names.
114
- const prefix = (await git(configDirectory, ["rev-parse", "--show-prefix"])).trim();
115
- return await gather(join(checkout, prefix, basename(configPath)));
116
- } finally {
117
- await git(top, ["worktree", "remove", "--force", checkout]).catch(() => {});
91
+ /**
92
+ * Read the network as it was at a commit: check that commit out beside the project, in `.circuit/cache/`, which git ignores, gather it there with `gather`, then remove the checkout. The config is the file of that name in the same directory, `configDirectory`.
93
+ *
94
+ * The checkout sits inside the project, so its imports of Circuit resolve to the version installed now.
95
+ */
96
+ static async networkAt(
97
+ configDirectory: string,
98
+ configPath: string,
99
+ commit: string,
100
+ gather: (configPath: string) => Promise<string>
101
+ ): Promise<string> {
102
+ const top = await Git.top(configDirectory);
103
+ if (!top) throw new Error("the config is not in a git repository");
104
+
105
+ const cache = join(configDirectory, ".circuit", "cache");
106
+ mkdirSync(cache, { recursive: true });
107
+ writeFileSync(join(cache, ".gitignore"), "# Written by Circuit Studio: checkouts of other commits, removed once read.\n*\n");
108
+
109
+ // A checkout left behind by a run that stopped is forgotten first.
110
+ await Git.run(top, ["worktree", "prune"]);
111
+
112
+ const checkout = join(cache, commit.slice(0, 12));
113
+ // The checkout sits deep inside the project, so a long path in the repository passes the 260 characters Windows allows unless git is told otherwise.
114
+ await Git.run(top, ["-c", "core.longpaths=true", "worktree", "add", "--detach", "--force", checkout, commit]);
115
+
116
+ try {
117
+ // Where the config sits inside the repository, as git says: comparing paths would trip on two spellings of one directory, such as Windows' short names.
118
+ const prefix = (await Git.run(configDirectory, ["rev-parse", "--show-prefix"])).trim();
119
+ return await gather(join(checkout, prefix, basename(configPath)));
120
+ } finally {
121
+ await Git.run(top, ["-c", "core.longpaths=true", "worktree", "remove", "--force", checkout]).catch(() => {});
122
+ }
118
123
  }
119
124
  }