@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.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11345 -9024
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
package/src/cli/new.ts
CHANGED
|
@@ -3,199 +3,232 @@
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import * as prompts from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
type Manager = keyof typeof Scaffold.MANAGERS;
|
|
10
|
+
|
|
11
|
+
type Pattern = keyof typeof Scaffold.PATTERNS;
|
|
12
|
+
|
|
13
|
+
/** The answers given as options. A question is asked only for an answer missing here. */
|
|
14
|
+
export type ScaffoldOptions = {
|
|
15
|
+
pattern?: string;
|
|
16
|
+
asn?: string;
|
|
17
|
+
onePassword?: boolean;
|
|
18
|
+
use?: string;
|
|
19
|
+
install: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class Scaffold {
|
|
23
|
+
private static readonly ROOT = Scaffold.packageRoot();
|
|
24
|
+
private static readonly TEMPLATES = join(Scaffold.ROOT, "templates");
|
|
25
|
+
private static readonly CIRCUIT = JSON.parse(readFileSync(join(Scaffold.ROOT, "package.json"), "utf8"));
|
|
26
|
+
|
|
27
|
+
/** How each package manager installs and runs. The templates are written for npm; the others are swapped in. */
|
|
28
|
+
static readonly MANAGERS = {
|
|
29
|
+
npm: { install: "npm install", run: "npx circuit", types: "node" },
|
|
30
|
+
pnpm: { install: "pnpm install", run: "pnpm circuit", types: "node" },
|
|
31
|
+
bun: { install: "bun install", run: "bun circuit", types: "bun" },
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
static readonly PATTERNS = {
|
|
35
|
+
"single-site": {
|
|
36
|
+
title: "A single site",
|
|
37
|
+
fits: "a home, an office or an internal network: a router and a switch, no BGP",
|
|
38
|
+
asn: false,
|
|
39
|
+
},
|
|
40
|
+
"edge-router": { title: "An edge router", fits: "your own AS number, with an IP transit and an internet exchange", asn: true },
|
|
41
|
+
colocation: {
|
|
42
|
+
title: "Colocation with tenants",
|
|
43
|
+
fits: "a provider whose customers each get their own VLANs, addresses and ports",
|
|
44
|
+
asn: true,
|
|
45
|
+
},
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
/** The AS number the templates use, reserved for documentation by RFC 5398. */
|
|
49
|
+
private static readonly EXAMPLE_ASN = "64500";
|
|
50
|
+
|
|
51
|
+
/** Circuit's own directory: this file runs from src/cli/ in Bun, and from dist/ once built. */
|
|
52
|
+
private static packageRoot(): string {
|
|
53
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
|
|
55
|
+
while (!existsSync(join(directory, "templates"))) {
|
|
56
|
+
const parent = dirname(directory);
|
|
57
|
+
if (parent === directory) throw new Error("Circuit's templates are missing from its package");
|
|
58
|
+
directory = parent;
|
|
59
|
+
}
|
|
8
60
|
|
|
9
|
-
|
|
10
|
-
function packageRoot(): string {
|
|
11
|
-
let directory = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
|
|
13
|
-
while (!existsSync(join(directory, "templates"))) {
|
|
14
|
-
const parent = dirname(directory);
|
|
15
|
-
if (parent === directory) throw new Error("Circuit's templates are missing from its package");
|
|
16
|
-
directory = parent;
|
|
61
|
+
return directory;
|
|
17
62
|
}
|
|
18
63
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/** How each package manager installs and runs. The templates are written for npm; the others are swapped in. */
|
|
27
|
-
const MANAGERS = {
|
|
28
|
-
npm: { install: "npm install", run: "npx circuit", types: "node" },
|
|
29
|
-
pnpm: { install: "pnpm install", run: "pnpm circuit", types: "node" },
|
|
30
|
-
bun: { install: "bun install", run: "bun circuit", types: "bun" },
|
|
31
|
-
} as const;
|
|
32
|
-
|
|
33
|
-
type Manager = keyof typeof MANAGERS;
|
|
34
|
-
|
|
35
|
-
/** The package manager that started this, from what it says about itself, such as `pnpm/9.1.0 npm/? node/v22`. */
|
|
36
|
-
function detectManager(): Manager {
|
|
37
|
-
const agent = process.env.npm_config_user_agent ?? "";
|
|
38
|
-
if (agent.startsWith("pnpm/")) return "pnpm";
|
|
39
|
-
if (agent.startsWith("bun/") || process.versions.bun) return "bun";
|
|
40
|
-
return "npm";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const PATTERNS = {
|
|
44
|
-
"single-site": { title: "A single site", fits: "a home, an office or an internal network: a router and a switch, no BGP", asn: false },
|
|
45
|
-
"edge-router": { title: "An edge router", fits: "your own AS number, with an IP transit and an internet exchange", asn: true },
|
|
46
|
-
colocation: {
|
|
47
|
-
title: "Colocation with tenants",
|
|
48
|
-
fits: "a provider whose customers each get their own VLANs, addresses and ports",
|
|
49
|
-
asn: true,
|
|
50
|
-
},
|
|
51
|
-
} as const;
|
|
52
|
-
|
|
53
|
-
type Pattern = keyof typeof PATTERNS;
|
|
54
|
-
|
|
55
|
-
/** The AS number the templates use, reserved for documentation by RFC 5398. */
|
|
56
|
-
const EXAMPLE_ASN = "64500";
|
|
64
|
+
/** The package manager that started this, from what it says about itself, such as `pnpm/9.1.0 npm/? node/v22`. */
|
|
65
|
+
private static detectManager(): Manager {
|
|
66
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
67
|
+
if (agent.startsWith("pnpm/")) return "pnpm";
|
|
68
|
+
if (agent.startsWith("bun/") || process.versions.bun) return "bun";
|
|
69
|
+
return "npm";
|
|
70
|
+
}
|
|
57
71
|
|
|
58
|
-
|
|
72
|
+
private static fail(message: string): never {
|
|
73
|
+
console.error(message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
/** Every file under a directory, relative to it. */
|
|
78
|
+
private static filesIn(directory: string): string[] {
|
|
79
|
+
return readdirSync(directory, { recursive: true, withFileTypes: true })
|
|
80
|
+
.filter((entry) => entry.isFile())
|
|
81
|
+
.map((entry) => relative(directory, join(entry.parentPath, entry.name)));
|
|
82
|
+
}
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
84
|
+
/** Why a project cannot be created in this directory, or undefined when it can. */
|
|
85
|
+
private static notEmpty(directory: string): string | undefined {
|
|
86
|
+
const target = resolve(process.cwd(), directory);
|
|
87
|
+
return existsSync(target) && readdirSync(target).length ? `${target} is not empty` : undefined;
|
|
88
|
+
}
|
|
70
89
|
|
|
71
|
-
/** The
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (at >= 0) return args[at + 1];
|
|
75
|
-
return args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
76
|
-
}
|
|
90
|
+
/** The answer to a question in a terminal, or the fallback when nobody is there to answer. Cancelling a question ends the run. */
|
|
91
|
+
private static async ask<T>(question: () => Promise<NoInfer<T> | symbol>, fallback: T): Promise<T> {
|
|
92
|
+
if (process.stdin.isTTY !== true) return fallback;
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
94
|
+
const answer = await question();
|
|
95
|
+
if (prompts.isCancel(answer)) {
|
|
96
|
+
prompts.cancel("Nothing created.");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
84
99
|
|
|
85
|
-
|
|
86
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
87
|
-
console.log(USAGE);
|
|
88
|
-
return;
|
|
100
|
+
return answer as T;
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
static async create(directoryArgument: string | undefined, options: ScaffoldOptions): Promise<void> {
|
|
104
|
+
const directory =
|
|
105
|
+
directoryArgument ??
|
|
106
|
+
(await Scaffold.ask(
|
|
107
|
+
() =>
|
|
108
|
+
prompts.text({
|
|
109
|
+
message: "Directory for the project",
|
|
110
|
+
placeholder: "my-network",
|
|
111
|
+
defaultValue: "my-network",
|
|
112
|
+
validate: (value) => Scaffold.notEmpty(value || "my-network"),
|
|
113
|
+
}),
|
|
114
|
+
"my-network"
|
|
115
|
+
));
|
|
116
|
+
|
|
117
|
+
const notEmpty = Scaffold.notEmpty(directory);
|
|
118
|
+
if (notEmpty) Scaffold.fail(notEmpty);
|
|
119
|
+
const target = resolve(process.cwd(), directory);
|
|
120
|
+
|
|
121
|
+
const names = Object.keys(Scaffold.PATTERNS) as Pattern[];
|
|
122
|
+
const pattern =
|
|
123
|
+
options.pattern ??
|
|
124
|
+
(await Scaffold.ask(
|
|
125
|
+
() =>
|
|
126
|
+
prompts.select({
|
|
127
|
+
message: "Which is closest to your network?",
|
|
128
|
+
options: names.map((name) => ({
|
|
129
|
+
value: name,
|
|
130
|
+
label: Scaffold.PATTERNS[name].title,
|
|
131
|
+
hint: Scaffold.PATTERNS[name].fits,
|
|
132
|
+
})),
|
|
133
|
+
}),
|
|
134
|
+
names[0]!
|
|
135
|
+
));
|
|
136
|
+
if (!Object.hasOwn(Scaffold.PATTERNS, pattern)) Scaffold.fail(`no pattern ${pattern}; there are ${names.join(", ")}`);
|
|
137
|
+
const chosen = Scaffold.PATTERNS[pattern as Pattern];
|
|
138
|
+
|
|
139
|
+
let asn = options.asn;
|
|
140
|
+
if (!asn && chosen.asn) {
|
|
141
|
+
asn = await Scaffold.ask(
|
|
142
|
+
() =>
|
|
143
|
+
prompts.text({
|
|
144
|
+
message: "Your AS number, or press enter to keep the documentation example",
|
|
145
|
+
placeholder: Scaffold.EXAMPLE_ASN,
|
|
146
|
+
defaultValue: Scaffold.EXAMPLE_ASN,
|
|
147
|
+
validate: (value) => (!value || /^\d+$/.test(value) ? undefined : `${value} is not an AS number`),
|
|
148
|
+
}),
|
|
149
|
+
Scaffold.EXAMPLE_ASN
|
|
150
|
+
);
|
|
114
151
|
}
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
if (!pattern || !(pattern in PATTERNS)) fail(`no pattern ${pattern}; there are ${Object.keys(PATTERNS).join(", ")}`);
|
|
152
|
+
if (asn && !/^\d+$/.test(asn)) Scaffold.fail(`${asn} is not an AS number`);
|
|
118
153
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.replaceAll("npm install", install)
|
|
144
|
-
.replaceAll('"types": ["node"]', `"types": ["${types}"]`);
|
|
145
|
-
writeFileSync(path, text);
|
|
146
|
-
}
|
|
154
|
+
const manager = options.use ?? Scaffold.detectManager();
|
|
155
|
+
if (!Object.hasOwn(Scaffold.MANAGERS, manager)) {
|
|
156
|
+
Scaffold.fail(`no package manager ${manager}; there are ${Object.keys(Scaffold.MANAGERS).join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
const { install, run, types } = Scaffold.MANAGERS[manager as Manager];
|
|
159
|
+
|
|
160
|
+
const onePassword =
|
|
161
|
+
options.onePassword ||
|
|
162
|
+
(await Scaffold.ask(() => prompts.confirm({ message: "Keep the secrets in 1Password?", initialValue: false }), false));
|
|
163
|
+
|
|
164
|
+
// The pattern's files, then what every project has. npm leaves out a file named .gitignore, so it travels without the dot.
|
|
165
|
+
cpSync(join(Scaffold.TEMPLATES, pattern), target, { recursive: true });
|
|
166
|
+
cpSync(join(Scaffold.TEMPLATES, "project"), target, { recursive: true });
|
|
167
|
+
renameSync(join(target, "gitignore"), join(target, ".gitignore"));
|
|
168
|
+
|
|
169
|
+
// What the templates say about npm, said for the package manager in use.
|
|
170
|
+
for (const file of ["AGENTS.md", "README.md", "tsconfig.json"]) {
|
|
171
|
+
const path = join(target, file);
|
|
172
|
+
const text = readFileSync(path, "utf8")
|
|
173
|
+
.replaceAll("npx circuit", run)
|
|
174
|
+
.replaceAll("npm install", install)
|
|
175
|
+
.replaceAll('"types": ["node"]', `"types": ["${types}"]`);
|
|
176
|
+
writeFileSync(path, text);
|
|
177
|
+
}
|
|
147
178
|
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
const sources = Scaffold.filesIn(target).filter((file) => file.endsWith(".ts"));
|
|
180
|
+
const secrets = new Set<string>();
|
|
150
181
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
182
|
+
for (const file of sources) {
|
|
183
|
+
const path = join(target, file);
|
|
184
|
+
let text = readFileSync(path, "utf8");
|
|
154
185
|
|
|
155
|
-
|
|
156
|
-
|
|
186
|
+
if (asn && asn !== Scaffold.EXAMPLE_ASN) text = text.replaceAll(new RegExp(`\\b${Scaffold.EXAMPLE_ASN}\\b`, "g"), asn);
|
|
187
|
+
for (const [, name] of text.matchAll(/secret\("([A-Z0-9_]+)"\)/g)) secrets.add(name!);
|
|
157
188
|
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
writeFileSync(path, text);
|
|
190
|
+
}
|
|
160
191
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
const env = [
|
|
193
|
+
"# Secrets for Circuit. Never commit this file.",
|
|
194
|
+
"# A value is the secret itself, or a 1Password reference, op://vault/item/field, read with the 1Password CLI.",
|
|
195
|
+
"# Put quotes around a value that has a # in it: without them, # starts a comment.",
|
|
196
|
+
...[...secrets].sort().map((name) => (onePassword ? `${name}=op://Network/${name}/password` : `${name}=`)),
|
|
197
|
+
"",
|
|
198
|
+
];
|
|
199
|
+
writeFileSync(join(target, ".env"), env.join("\n"));
|
|
200
|
+
|
|
201
|
+
const name = basename(target)
|
|
202
|
+
.toLowerCase()
|
|
203
|
+
.replace(/[^a-z0-9-]+/g, "-");
|
|
204
|
+
const packageJson = {
|
|
205
|
+
name,
|
|
206
|
+
private: true,
|
|
207
|
+
type: "module",
|
|
208
|
+
dependencies: { "@takodotid/circuit": Scaffold.CIRCUIT.version },
|
|
209
|
+
devDependencies: { [`@types/${types}`]: Scaffold.CIRCUIT.devDependencies[`@types/${types}`] },
|
|
210
|
+
};
|
|
211
|
+
writeFileSync(join(target, "package.json"), JSON.stringify(packageJson, null, 4) + "\n");
|
|
212
|
+
|
|
213
|
+
if (options.install) {
|
|
214
|
+
// npm and pnpm are scripts on Windows, which only a shell runs.
|
|
215
|
+
const result = spawnSync(install, { cwd: target, stdio: "inherit", shell: true });
|
|
216
|
+
if (result.status !== 0) console.log(`${install} failed; run it again in the project.`);
|
|
217
|
+
}
|
|
186
218
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
219
|
+
const steps = [
|
|
220
|
+
`cd ${directory}`,
|
|
221
|
+
onePassword
|
|
222
|
+
? `Point each line of .env at the item that holds it in 1Password, then run: ${run} secrets`
|
|
223
|
+
: `Fill in each password in .env, then run: ${run} secrets`,
|
|
224
|
+
"Change the names, addresses and ports in the device files to your own.",
|
|
225
|
+
...(chosen.asn && (!asn || asn === Scaffold.EXAMPLE_ASN) ? ["Put your own AS number in routing.ts."] : []),
|
|
226
|
+
`${run} validate`,
|
|
227
|
+
`${run} snapshot, then ${run} diff, to see what would change on each device`,
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
console.log(`\nCreated ${directory} from the pattern "${chosen.title}".\n\nNext:`);
|
|
231
|
+
steps.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
|
|
232
|
+
console.log(`\nWhat each file does: https://circuit.tako.id/patterns/${pattern}`);
|
|
233
|
+
}
|
|
201
234
|
}
|
package/src/cli/rename.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { ProjectFiles } from "../core/project-files";
|
|
6
6
|
|
|
7
7
|
/** One line that changes. */
|
|
8
8
|
export type LineChange = { file: string; line: number; before: string; after: string };
|
|
@@ -15,115 +15,121 @@ export type RenamePlan = {
|
|
|
15
15
|
mentions: { file: string; line: number; text: string }[];
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
export class Rename {
|
|
19
|
+
private static escape(text: string): string {
|
|
20
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
}
|
|
19
22
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Plan renaming a device from `from` to `to` in the project at `root`.
|
|
25
|
+
*
|
|
26
|
+
* A string that is exactly the old name becomes the new name: `name: "edge-01"`, `link: { device: "edge-01", ... }`, a check's list of routers, and a key such as a tenant's ports by device, which would otherwise no longer be found. A BGP neighbor named after the device is renamed too, which renames its session on the device: `circuit diff` shows it.
|
|
27
|
+
*/
|
|
28
|
+
static plan(root: string, from: string, to: string, options: { file: boolean; snapshots: string[] }): RenamePlan {
|
|
29
|
+
const changes: LineChange[] = [];
|
|
30
|
+
const moves: RenamePlan["moves"] = [];
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const exact = new RegExp(`(["'\`])${Rename.escape(from)}\\1`, "g");
|
|
33
|
+
const anywhere = new RegExp(`(^|[^A-Za-z0-9_.-])${Rename.escape(from)}(?![A-Za-z0-9_-])`);
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const sources = ProjectFiles.sources(root);
|
|
36
|
+
const declaring = new Set<string>();
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
for (const file of sources) {
|
|
39
|
+
const text = readFileSync(join(root, file), "utf8");
|
|
40
|
+
const lines = text.split("\n");
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
// The device's own file calls defineDevice and gives it the name.
|
|
43
|
+
if (ProjectFiles.declaresDevice(text, from)) declaring.add(file);
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
for (const [index, before] of lines.entries()) {
|
|
46
|
+
const after = before.replace(exact, (_match, quote: string) => `${quote}${to}${quote}`);
|
|
47
|
+
if (after !== before) changes.push({ file, line: index + 1, before, after });
|
|
48
|
+
}
|
|
45
49
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!declaring.size) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`no file declares name: "${from}"; the name may be built from parts, such as a template string, so rename it by hand`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
if (!declaring.size) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`no file declares name: "${from}"; the name may be built from parts, such as a template string, so rename it by hand`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
for (const snapshot of options.snapshots) {
|
|
58
|
+
// A state file is named after the device, then its kind: `tor-01.cfg`, `tor-01.lldp.json`.
|
|
59
|
+
moves.push({ from: snapshot, to: join(dirname(snapshot), `${to}${basename(snapshot).slice(from.length)}`) });
|
|
60
|
+
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (options.file) {
|
|
63
|
+
if (declaring.size > 1)
|
|
64
|
+
throw new Error(`${[...declaring].join(" and ")} both declare name: "${from}"; rename the file by hand`);
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const [file] = [...declaring] as [string];
|
|
67
|
+
const target = join(dirname(file), `${to}${extname(file)}`);
|
|
68
|
+
if (existsSync(join(root, target))) throw new Error(`${target} already exists`);
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.split("\n")
|
|
73
|
-
.flatMap((original, index) => {
|
|
74
|
-
const text = changes.find((change) => change.file === file && change.line === index + 1)?.after ?? original;
|
|
75
|
-
return anywhere.test(text) ? [{ file, line: index + 1, text: text.trim() }] : [];
|
|
76
|
-
})
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return { changes, moves, mentions };
|
|
80
|
-
}
|
|
70
|
+
moves.push({ from: file, to: target });
|
|
71
|
+
changes.push(...Rename.importChanges(root, sources, file, target, changes));
|
|
72
|
+
}
|
|
81
73
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
for (const [index, original] of lines.entries()) {
|
|
92
|
-
const line = index + 1;
|
|
93
|
-
const changed = earlier.find((change) => change.file === source && change.line === line);
|
|
94
|
-
const before = changed?.after ?? original;
|
|
95
|
-
|
|
96
|
-
// A relative specifier that resolves to the moved file keeps its own form, extension or not.
|
|
97
|
-
const after = before.replace(
|
|
98
|
-
/(\bfrom\s+|\bimport\s*\(\s*)(["'])(\.{1,2}\/[^"']+)\2/g,
|
|
99
|
-
(match, lead: string, quote: string, specifier: string) => {
|
|
100
|
-
if (withoutExtension(resolve(root, dirname(source), specifier)) !== moved) return match;
|
|
101
|
-
|
|
102
|
-
const extension = specifier.match(/\.(ts|mts|cts|js|mjs|cjs)$/)?.[0] ?? "";
|
|
103
|
-
const renamed = `${dirname(specifier)}/${basename(withoutExtension(target))}${extension}`;
|
|
104
|
-
return `${lead}${quote}${renamed}${quote}`;
|
|
105
|
-
}
|
|
106
|
-
);
|
|
74
|
+
// What still names the device once every change is made.
|
|
75
|
+
const mentions = sources.flatMap((file) =>
|
|
76
|
+
readFileSync(join(root, file), "utf8")
|
|
77
|
+
.split("\n")
|
|
78
|
+
.flatMap((original, index) => {
|
|
79
|
+
const text = changes.find((change) => change.file === file && change.line === index + 1)?.after ?? original;
|
|
80
|
+
return anywhere.test(text) ? [{ file, line: index + 1, text: text.trim() }] : [];
|
|
81
|
+
})
|
|
82
|
+
);
|
|
107
83
|
|
|
108
|
-
|
|
84
|
+
return { changes, moves, mentions };
|
|
85
|
+
}
|
|
109
86
|
|
|
110
|
-
|
|
111
|
-
|
|
87
|
+
/** Imports of a file that moves, pointed at its new name. Lines already changed are changed further. */
|
|
88
|
+
private static importChanges(root: string, sources: string[], file: string, target: string, earlier: LineChange[]): LineChange[] {
|
|
89
|
+
const result: LineChange[] = [];
|
|
90
|
+
const withoutExtension = (path: string) => path.replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
|
|
91
|
+
const moved = withoutExtension(resolve(root, file));
|
|
92
|
+
|
|
93
|
+
for (const source of sources) {
|
|
94
|
+
const lines = readFileSync(join(root, source), "utf8").split("\n");
|
|
95
|
+
|
|
96
|
+
for (const [index, original] of lines.entries()) {
|
|
97
|
+
const line = index + 1;
|
|
98
|
+
const changed = earlier.find((change) => change.file === source && change.line === line);
|
|
99
|
+
const before = changed?.after ?? original;
|
|
100
|
+
|
|
101
|
+
// A relative specifier that resolves to the moved file keeps its own form, extension or not.
|
|
102
|
+
const after = before.replace(
|
|
103
|
+
/(\bfrom\s+|\bimport\s*\(\s*)(["'])(\.{1,2}\/[^"']+)\2/g,
|
|
104
|
+
(match, lead: string, quote: string, specifier: string) => {
|
|
105
|
+
if (withoutExtension(resolve(root, dirname(source), specifier)) !== moved) return match;
|
|
106
|
+
|
|
107
|
+
const extension = specifier.match(/\.(ts|mts|cts|js|mjs|cjs)$/)?.[0] ?? "";
|
|
108
|
+
const renamed = `${dirname(specifier)}/${basename(withoutExtension(target))}${extension}`;
|
|
109
|
+
return `${lead}${quote}${renamed}${quote}`;
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (after === before) continue;
|
|
114
|
+
|
|
115
|
+
if (changed) changed.after = after;
|
|
116
|
+
else result.push({ file: source, line, before: original, after });
|
|
117
|
+
}
|
|
112
118
|
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
/** Write the plan: each changed line, then each move. */
|
|
124
|
+
static apply(root: string, plan: RenamePlan): void {
|
|
125
|
+
const files = new Set(plan.changes.map((change) => change.file));
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const lines = readFileSync(join(root, file), "utf8").split("\n");
|
|
129
|
+
for (const change of plan.changes.filter((each) => each.file === file)) lines[change.line - 1] = change.after;
|
|
130
|
+
writeFileSync(join(root, file), lines.join("\n"));
|
|
131
|
+
}
|
|
121
132
|
|
|
122
|
-
|
|
123
|
-
const lines = readFileSync(join(root, file), "utf8").split("\n");
|
|
124
|
-
for (const change of plan.changes.filter((each) => each.file === file)) lines[change.line - 1] = change.after;
|
|
125
|
-
writeFileSync(join(root, file), lines.join("\n"));
|
|
133
|
+
for (const move of plan.moves) renameSync(join(root, move.from), join(root, move.to));
|
|
126
134
|
}
|
|
127
|
-
|
|
128
|
-
for (const move of plan.moves) renameSync(join(root, move.from), join(root, move.to));
|
|
129
135
|
}
|