@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.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
package/src/studio/data.ts
CHANGED
|
@@ -1,112 +1,65 @@
|
|
|
1
|
-
// What Studio shows, gathered from
|
|
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 {
|
|
4
|
-
import {
|
|
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 {
|
|
9
|
-
import { Topology
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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[]
|
|
153
|
-
const
|
|
154
|
-
|
|
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(({
|
|
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 !==
|
|
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.
|
|
185
|
-
ownerOf(other.address) ===
|
|
123
|
+
other.device === far &&
|
|
124
|
+
ownerOf(other.address) === device &&
|
|
186
125
|
Address.family(other.address) === Address.family(session.address)
|
|
187
126
|
);
|
|
188
|
-
return { ...
|
|
127
|
+
return { ...base, far: far.name, ...(back ? {} : { oneSided: true as const }) };
|
|
189
128
|
}
|
|
190
129
|
|
|
191
|
-
return { ...
|
|
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
|
|
213
|
-
const link = port
|
|
214
|
-
if (!
|
|
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)?.
|
|
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.
|
|
230
|
-
...(farPort?.
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
46
|
-
static async top(
|
|
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(
|
|
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,
|
|
55
|
-
static async history(
|
|
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(
|
|
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(
|
|
52
|
+
static async references(folder: string): Promise<{ names: string[]; commits: Commit[] }> {
|
|
62
53
|
const names = (
|
|
63
|
-
await Git.run(
|
|
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(
|
|
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
|
|
80
|
-
static async commitOf(
|
|
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(
|
|
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
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
|
118
|
-
const prefix = (await Git.run(
|
|
119
|
-
return await gather(join(checkout, prefix
|
|
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
|
}
|