@takodotid/circuit 0.43.1 → 1.0.0-rc.2

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 (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -1,112 +1,65 @@
1
- // What Studio shows, gathered from the config in one pass: the graph with its findings and drift, each device as the config holds it, what each would run, the address plan, every BGP session and cable, and the schema's descriptions of each field.
1
+ // What Studio shows, gathered from a project in one pass: each device as the model holds it, what each would run, what `validate` finds, the graph, the addresses, every BGP session and every cable.
2
2
 
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { Catalog } from "../platforms/catalog";
3
+ import { basename } from "node:path";
4
+ import { Validation } from "../checks";
7
5
  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
- import type { Device } from "../schema";
12
-
13
- /** One BGP session, with its group's settings filled in, and what is at its far end. */
14
- export type Session = {
15
- device: string;
16
- name: string;
17
- address: string;
18
- group?: string;
19
- remoteAs?: number;
20
- role?: string;
21
- import?: string;
22
- export?: string;
23
- description?: string;
24
- /** One of our devices, by name, or another network, as `AS64501`. */
25
- far?: string;
26
- /** The far device has no session back. */
27
- oneSided?: true;
28
- };
29
-
30
- /** One cable, from the end that names it first. */
31
- export type Cable = {
32
- device: string;
33
- port: string;
34
- farDevice: string;
35
- farPort: string;
36
- /** The LAG each end's port is in. */
37
- lag?: string;
38
- farLag?: string;
39
- description?: string;
40
- /** What is between the two ends, such as a carrier's local loop. */
41
- through?: string;
42
- /** VLANs carried, as `name (id)`, from the port or its LAG. */
43
- vlans: string[];
44
- /** The far end does not name this cable back. */
45
- oneSided?: true;
46
- };
47
-
48
- /** A schema type's description and its fields' descriptions, by field path. */
49
- export type TypeDocs = {
50
- description: string;
51
- includes: string[];
52
- fields: Record<string, { type: string; description: string }>;
53
- };
54
-
55
- export type StudioData = {
56
- graph: Annotated;
57
- devices: readonly Device[];
58
- /** Every port each device's model has, in order, so Studio can show the ones the config leaves shut. */
59
- ports: Record<string, string[]>;
60
- /** What each device would run, as `circuit build` prints it, or why it cannot be rendered. */
61
- rendered: Record<string, string>;
62
- /** What each device ran when Circuit last read it, secrets removed, for the devices with a snapshot. */
63
- snapshots: Record<string, string>;
64
- /** Each device's file and snapshot, relative to the config, for its history. */
65
- files: Record<string, DeviceFiles>;
66
- addresses: AddressReport;
67
- sessions: Session[];
68
- cables: Cable[];
69
- /** By type name. */
70
- docs: Record<string, TypeDocs>;
71
- };
6
+ import { Secrets } from "../core/secrets";
7
+ import { Topology } from "../core/topology";
8
+ import type { Device } from "../model";
9
+ import { Platforms } from "../platforms";
10
+ import { Catalog } from "../platforms/catalog";
11
+ import { Derive } from "../platforms/derive";
12
+ import type { Finding } from "../platforms/finding";
13
+ import { ProjectError, type ProjectRead, readProject, secretsFileOf } from "../project/project";
14
+ import type { AddressRow, Cable, Session, StudioData, StudioError } from "./types";
72
15
 
73
- /** 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. */
74
- export type DeviceFiles = { source?: string; snapshot?: string };
16
+ export class StudioModel {
17
+ /** Read the project and gather what Studio shows, or what stopped the read. */
18
+ static async gather(root: string): Promise<StudioData | StudioError> {
19
+ let read: ProjectRead;
20
+ try {
21
+ read = await readProject(root);
22
+ } catch (error) {
23
+ if (error instanceof ProjectError) return { error: error.lines.join("\n") };
24
+ return { error: (error as Error).message };
25
+ }
75
26
 
76
- /** How Studio learns what a device would run and what it ran, from the caller, which knows the adapters and the state directory. */
77
- export type DeviceTexts = {
78
- /** What the device would run, as `circuit build` prints it. */
79
- render: (device: Device) => string;
80
- /** Its last snapshot, or undefined when it has none. */
81
- snapshot: (device: Device) => string | undefined;
82
- /** Its file and its snapshot's file. */
83
- files: (device: Device) => DeviceFiles;
84
- };
27
+ return StudioModel.build(basename(root), read);
28
+ }
85
29
 
86
- export class StudioModel {
87
- /** Everything Studio shows. `graph` comes ready from the caller, with each device compared with its snapshot. */
88
- static build(devices: readonly Device[], graph: Annotated, texts: DeviceTexts): StudioData {
89
- const owners = new Map<string, Holder>();
90
- for (const holder of Topology.holders(devices)) {
91
- const key = Topology.keyOf(Address.host(holder.address))!;
92
- if (!owners.has(key)) owners.set(key, holder);
30
+ private static attempt<T>(work: () => T, otherwise: (error: Error) => T): T {
31
+ try {
32
+ return work();
33
+ } catch (error) {
34
+ return otherwise(error as Error);
93
35
  }
36
+ }
94
37
 
95
- const attempt = <T>(work: () => T, otherwise: (error: Error) => T) => {
96
- try {
97
- return work();
98
- } catch (error) {
99
- return otherwise(error as Error);
100
- }
101
- };
38
+ static build(project: string, read: ProjectRead): StudioData {
39
+ const devices = read.networks.flatMap((network) => Object.values(network.devices));
40
+ const findings: Finding[] = [
41
+ ...read.warnings.map((message) => ({ level: "warning" as const, message })),
42
+ ...read.networks.flatMap((network) => Validation.run(Object.values(network.devices), network.settings.checks)),
43
+ ];
102
44
 
103
45
  return {
104
- graph,
46
+ project,
47
+ networks: read.networks.map((network) => ({
48
+ name: network.name,
49
+ ...(network.asn !== undefined ? { asn: network.asn } : {}),
50
+ sites: network.settings.sites ?? {},
51
+ devices: Object.keys(network.devices),
52
+ secretsFile: secretsFileOf(network),
53
+ secrets: [...Secrets.referencesIn([network.devices, network.settings.integrations])]
54
+ .filter((name) => !name.startsWith("file:"))
55
+ .sort(),
56
+ })),
105
57
  devices,
58
+ files: Object.fromEntries(read.networks.flatMap((network) => Object.entries(network.files))),
106
59
  ports: Object.fromEntries(
107
60
  devices.map((device) => [
108
61
  device.name,
109
- attempt(
62
+ StudioModel.attempt(
110
63
  () => Catalog.ports(device.platform, device.model),
111
64
  () => []
112
65
  ),
@@ -115,80 +68,66 @@ export class StudioModel {
115
68
  rendered: Object.fromEntries(
116
69
  devices.map((device) => [
117
70
  device.name,
118
- attempt(
119
- () => texts.render(device),
71
+ StudioModel.attempt(
72
+ () => Platforms.find(device.platform)?.render(device) ?? `Cannot be rendered: no platform ${device.platform}`,
120
73
  (error) => `Cannot be rendered: ${error.message}`
121
74
  ),
122
75
  ])
123
76
  ),
124
- snapshots: Object.fromEntries(
125
- devices.flatMap((device) => {
126
- const text = texts.snapshot(device);
127
- return text === undefined ? [] : [[device.name, text]];
128
- })
129
- ),
130
- files: Object.fromEntries(devices.map((device) => [device.name, texts.files(device)])),
131
- addresses: AddressSpace.report(devices),
132
- sessions: StudioModel.sessionsOf(devices, owners),
77
+ findings,
78
+ graph: Topology.of(devices),
79
+ addresses: StudioModel.addressesOf(devices),
80
+ sessions: StudioModel.sessionsOf(devices),
133
81
  cables: StudioModel.cablesOf(devices),
134
- docs: StudioModel.schemaDocs(),
82
+ secrets: [...Secrets.referencesIn(read.networks.map((network) => [network.devices, network.settings.integrations]))]
83
+ .filter((name) => !name.startsWith("file:"))
84
+ .sort(),
135
85
  };
136
86
  }
137
87
 
138
- /** 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. */
139
- static packageRoot(): string | undefined {
140
- let directory = dirname(fileURLToPath(import.meta.url));
141
-
142
- while (true) {
143
- const manifest = join(directory, "package.json");
144
- if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@takodotid/circuit") return directory;
145
-
146
- const parent = dirname(directory);
147
- if (parent === directory) return undefined;
148
- directory = parent;
149
- }
88
+ private static addressesOf(devices: readonly Device[]): AddressRow[] {
89
+ return Topology.holders(devices).map((holder) => ({
90
+ device: holder.device.name,
91
+ holder: holder.name,
92
+ address: holder.address,
93
+ prefix: Address.prefixOf(holder.address),
94
+ family: Address.family(holder.address),
95
+ ...(holder.peer ? { peer: holder.peer } : {}),
96
+ ...(holder.vrf ? { vrf: holder.vrf } : {}),
97
+ }));
150
98
  }
151
99
 
152
- private static sessionsOf(devices: readonly Device[], owners: Map<string, Holder>): Session[] {
153
- const all = devices.flatMap((device) => {
154
- const bgp = device.routing?.bgp;
155
-
156
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]): Session & { deviceObject: Device } => {
157
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
158
- const settings = { ...group, ...neighbor };
159
-
160
- return {
161
- deviceObject: device,
162
- device: device.name,
163
- name,
164
- address: neighbor.address,
165
- group: neighbor.group,
166
- remoteAs: settings.remote_as,
167
- role: settings.role,
168
- import: settings.import,
169
- export: settings.export,
170
- description: settings.description,
171
- };
172
- });
173
- });
174
-
100
+ private static sessionsOf(devices: readonly Device[]): Session[] {
101
+ const owners = Topology.owners(Topology.holders(devices));
102
+ const all = Topology.sessionsOf(devices);
175
103
  const ownerOf = (address: string) => owners.get(Topology.keyOf(address) ?? "")?.device;
176
104
 
177
- return all.map(({ deviceObject, ...session }) => {
105
+ return all.map(({ device, ownAs: _ownAs, ...session }) => {
106
+ const base: Session = {
107
+ device: device.name,
108
+ name: session.name,
109
+ address: session.address,
110
+ ...(session.group ? { group: session.group } : {}),
111
+ ...(session.remoteAs !== undefined ? { remoteAs: session.remoteAs } : {}),
112
+ ...(session.role ? { role: session.role } : {}),
113
+ ...(session.import ? { import: session.import } : {}),
114
+ ...(session.export ? { export: session.export } : {}),
115
+ ...(session.description ? { description: session.description } : {}),
116
+ };
178
117
  const far = ownerOf(session.address);
179
118
 
180
- if (far && far !== deviceObject) {
119
+ if (far && far !== device) {
181
120
  // The session back is one of the same family: a v4 session back does not answer a v6 session.
182
121
  const back = all.some(
183
122
  (other) =>
184
- other.deviceObject === far &&
185
- ownerOf(other.address) === deviceObject &&
123
+ other.device === far &&
124
+ ownerOf(other.address) === device &&
186
125
  Address.family(other.address) === Address.family(session.address)
187
126
  );
188
- return { ...session, far: far.name, ...(back ? {} : { oneSided: true as const }) };
127
+ return { ...base, far: far.name, ...(back ? {} : { oneSided: true as const }) };
189
128
  }
190
129
 
191
- return { ...session, ...(session.remoteAs ? { far: `AS${session.remoteAs}` } : {}) };
130
+ return { ...base, ...(session.remoteAs ? { far: `AS${session.remoteAs}` } : {}) };
192
131
  });
193
132
  }
194
133
 
@@ -197,40 +136,30 @@ export class StudioModel {
197
136
  const drawn = new Set<string>();
198
137
  const cables: Cable[] = [];
199
138
 
200
- const vlansOf = (device: Device, portName: string) => {
201
- const port = device.ports?.[portName];
202
- const lag = port?.lag ? device.interfaces?.[port.lag] : undefined;
203
- const carrier = lag?.type === "lag" ? lag : port;
204
- const names = [carrier?.access_vlan, carrier?.native_vlan, ...(carrier?.trunk_vlans ?? [])].filter(
205
- (name) => name !== undefined
206
- );
207
-
208
- return [...new Set(names)].map((name) => `${name} (${device.vlans?.[name]?.id ?? "?"})`);
209
- };
210
-
211
139
  for (const device of devices) {
212
- for (const [portName, port] of Object.entries(device.ports ?? {})) {
213
- const link = port?.link;
214
- if (!port || !link) continue;
140
+ for (const [portName, port] of Derive.ports(device)) {
141
+ const link = port.link;
142
+ if (!link) continue;
215
143
 
216
144
  const here = `${device.name} ${portName}`;
217
145
  const there = `${link.device} ${link.port}`;
218
146
  if (drawn.has(`${there}|${here}`)) continue;
219
147
  drawn.add(`${here}|${there}`);
220
148
 
221
- const farPort = byName.get(link.device)?.ports?.[link.port];
149
+ const farPort = byName.get(link.device)?.interfaces?.ethernet?.[link.port];
222
150
  const linksBack = farPort?.link?.device === device.name && farPort.link.port === portName;
151
+ const vlans = Topology.carriedBy(device, Topology.carrierOf(device, port)).map((vlan) => `${vlan.name} (${vlan.id})`);
223
152
 
224
153
  cables.push({
225
154
  device: device.name,
226
155
  port: portName,
227
156
  farDevice: link.device,
228
157
  farPort: link.port,
229
- ...(port.lag ? { lag: port.lag } : {}),
230
- ...(farPort?.lag ? { farLag: farPort.lag } : {}),
158
+ ...(port.in_lag ? { lag: port.in_lag } : {}),
159
+ ...(farPort?.in_lag ? { farLag: farPort.in_lag } : {}),
231
160
  ...(port.description ? { description: port.description } : {}),
232
161
  ...(link.through ? { through: link.through } : {}),
233
- vlans: vlansOf(device, portName),
162
+ vlans,
234
163
  ...(linksBack ? {} : { oneSided: true as const }),
235
164
  });
236
165
  }
@@ -238,25 +167,4 @@ export class StudioModel {
238
167
 
239
168
  return cables;
240
169
  }
241
-
242
- /** The schema's descriptions, or none when its sources cannot be found. */
243
- private static schemaDocs(): Record<string, TypeDocs> {
244
- const root = StudioModel.packageRoot();
245
- if (!root) return {};
246
-
247
- const sources = SchemaDocs.FILES.map(([file]) => readFileSync(join(root, "src", "schema", file), "utf8"));
248
- const docs: Record<string, TypeDocs> = {};
249
-
250
- for (const type of SchemaDocs.read(sources).flat()) {
251
- docs[type.name] = {
252
- description: type.description,
253
- includes: (type.includes ?? []).map((name) => name.replace(/<.*$/, "")),
254
- fields: Object.fromEntries(
255
- type.fields.map((field) => [field.path.replace(/\?$/, ""), { type: field.type, description: field.description }])
256
- ),
257
- };
258
- }
259
-
260
- return docs;
261
- }
262
170
  }
package/src/studio/git.ts CHANGED
@@ -1,28 +1,19 @@
1
- // Git, for Studio's history and comparisons: the commits that changed a device, and the network as it was at another commit or branch.
1
+ // Git, for Studio's history and comparisons: the commits that changed a device's file, and the project as it was at another commit or branch.
2
2
 
3
3
  import { execFile } from "node:child_process";
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
5
- import { basename, join } from "node:path";
6
-
7
- /** One commit, as Studio lists it. */
8
- export type Commit = {
9
- commit: string;
10
- short: string;
11
- author: string;
12
- /** ISO 8601, with the author's time zone. */
13
- date: string;
14
- subject: string;
15
- };
5
+ import { join } from "node:path";
6
+ import type { Commit } from "./types";
16
7
 
17
8
  export class Git {
18
9
  private static readonly FORMAT = "--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s";
19
10
 
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. */
11
+ /** The environment without git's own variables, such as GIT_DIR in a hook, which would point git at another repository than the folder's. */
21
12
  private static environment(): Record<string, string | undefined> {
22
13
  return Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_")));
23
14
  }
24
15
 
25
- /** Run git in a directory, and give what it printed, or fail with what it said. */
16
+ /** Run git in a folder, and give what it printed, or fail with what it said. */
26
17
  private static run(cwd: string, args: string[]): Promise<string> {
27
18
  return new Promise((done, fail) => {
28
19
  execFile("git", args, { cwd, env: Git.environment(), maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
@@ -42,25 +33,25 @@ export class Git {
42
33
  });
43
34
  }
44
35
 
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> {
36
+ /** The top of the git repository the folder is in, or undefined when it is in none, or git is missing. */
37
+ static async top(folder: string): Promise<string | undefined> {
47
38
  try {
48
- return (await Git.run(directory, ["rev-parse", "--show-toplevel"])).trim();
39
+ return (await Git.run(folder, ["rev-parse", "--show-toplevel"])).trim();
49
40
  } catch {
50
41
  return undefined;
51
42
  }
52
43
  }
53
44
 
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[]> {
45
+ /** The latest commits that changed any of these files, from the folder, newest first. */
46
+ static async history(folder: string, files: string[], limit = 50): Promise<Commit[]> {
56
47
  if (!files.length) return [];
57
- return Git.commitsOf(await Git.run(directory, ["log", `-n${limit}`, Git.FORMAT, "--", ...files]));
48
+ return Git.commitsOf(await Git.run(folder, ["log", `-n${limit}`, Git.FORMAT, "--", ...files]));
58
49
  }
59
50
 
60
51
  /** Branches and tags to compare with, and the latest commits. */
61
- static async references(directory: string): Promise<{ names: string[]; commits: Commit[] }> {
52
+ static async references(folder: string): Promise<{ names: string[]; commits: Commit[] }> {
62
53
  const names = (
63
- await Git.run(directory, [
54
+ await Git.run(folder, [
64
55
  "for-each-ref",
65
56
  "--count=100",
66
57
  "--sort=-committerdate",
@@ -73,15 +64,15 @@ export class Git {
73
64
  .split("\n")
74
65
  .filter((name) => name && !name.endsWith("/HEAD"));
75
66
 
76
- return { names, commits: Git.commitsOf(await Git.run(directory, ["log", "-n30", Git.FORMAT])) };
67
+ return { names, commits: Git.commitsOf(await Git.run(folder, ["log", "-n30", Git.FORMAT])) };
77
68
  }
78
69
 
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> {
70
+ /** 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 taken. */
71
+ static async commitOf(folder: string, name: string): Promise<Commit | undefined> {
81
72
  if (!/^[A-Za-z0-9][\w./@^~-]*$/.test(name)) return undefined;
82
73
 
83
74
  try {
84
- const [commit] = Git.commitsOf(await Git.run(directory, ["log", "-n1", Git.FORMAT, `${name}^{commit}`, "--"]));
75
+ const [commit] = Git.commitsOf(await Git.run(folder, ["log", "-n1", Git.FORMAT, `${name}^{commit}`, "--"]));
85
76
  return commit;
86
77
  } catch {
87
78
  return undefined;
@@ -89,20 +80,13 @@ export class Git {
89
80
  }
90
81
 
91
82
  /**
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.
83
+ * Read the project 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. It is read by the Circuit running now, so a comparison shows changes to the project, not to Circuit.
95
84
  */
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");
85
+ static async projectAt(root: string, commit: string, gather: (folder: string) => Promise<string>): Promise<string> {
86
+ const top = await Git.top(root);
87
+ if (!top) throw new Error("the project is not in a git repository");
88
+
89
+ const cache = join(root, ".circuit", "cache");
106
90
  mkdirSync(cache, { recursive: true });
107
91
  writeFileSync(join(cache, ".gitignore"), "# Written by Circuit Studio: checkouts of other commits, removed once read.\n*\n");
108
92
 
@@ -110,13 +94,13 @@ export class Git {
110
94
  await Git.run(top, ["worktree", "prune"]);
111
95
 
112
96
  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.
97
+ // The checkout sits deep inside the project, so a long path passes the 260 characters Windows allows unless git is told otherwise.
114
98
  await Git.run(top, ["-c", "core.longpaths=true", "worktree", "add", "--detach", "--force", checkout, commit]);
115
99
 
116
100
  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)));
101
+ // Where the project sits inside the repository, as git says: comparing paths would trip on two spellings of one folder, such as Windows' short names.
102
+ const prefix = (await Git.run(root, ["rev-parse", "--show-prefix"])).trim();
103
+ return await gather(join(checkout, prefix));
120
104
  } finally {
121
105
  await Git.run(top, ["-c", "core.longpaths=true", "worktree", "remove", "--force", checkout]).catch(() => {});
122
106
  }