@takodotid/circuit 0.9.0 → 0.10.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 +3 -2
- package/dist/chunks/index-ex1wcst7.js +109 -0
- package/dist/chunks/index-fq57hya4.js +20 -0
- package/dist/chunks/new-7n8qvtjd.js +168 -0
- package/dist/cli/index.js +4079 -0
- package/dist/index.js +48 -0
- package/dist/presets/index.js +257 -0
- package/dist/types/adapters/devices/catalog.d.ts +58 -0
- package/dist/types/adapters/devices/derive.d.ts +5 -0
- package/dist/types/adapters/devices/index.d.ts +3 -0
- package/dist/types/adapters/devices/lines.d.ts +56 -0
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +23 -0
- package/dist/types/adapters/devices/raisecom-ros/models.d.ts +14 -0
- package/dist/types/adapters/devices/routeros/export.d.ts +39 -0
- package/dist/types/adapters/devices/routeros/index.d.ts +17 -0
- package/dist/types/adapters/devices/routeros/models.d.ts +14 -0
- package/dist/types/adapters/devices/routeros/plan.d.ts +4 -0
- package/dist/types/adapters/devices/routeros/render.d.ts +96 -0
- package/dist/types/adapters/devices/types.d.ts +34 -0
- package/dist/types/adapters/devices/vrp/index.d.ts +24 -0
- package/dist/types/adapters/devices/vrp/models.d.ts +12 -0
- package/dist/types/adapters/registries/index.d.ts +3 -0
- package/dist/types/adapters/registries/peeringdb.d.ts +16 -0
- package/dist/types/core/addr.d.ts +24 -0
- package/dist/types/core/define.d.ts +50 -0
- package/dist/types/core/merge.d.ts +4 -0
- package/dist/types/core/secrets.d.ts +21 -0
- package/dist/types/core/time.d.ts +2 -0
- package/dist/types/core/validate.d.ts +2 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/presets/index.d.ts +99 -0
- package/dist/types/schema/common.d.ts +24 -0
- package/dist/types/schema/device.d.ts +80 -0
- package/dist/types/schema/firewall.d.ts +118 -0
- package/dist/types/schema/index.d.ts +7 -0
- package/dist/types/schema/interfaces.d.ts +181 -0
- package/dist/types/schema/policy.d.ts +88 -0
- package/dist/types/schema/routing.d.ts +115 -0
- package/dist/types/schema/system.d.ts +186 -0
- package/dist/types/transport/ssh.d.ts +22 -0
- package/package.json +19 -5
- package/src/cli/index.ts +10 -2
- package/src/cli/new.ts +60 -12
- package/src/core/secrets.ts +7 -9
- package/templates/project/AGENTS.md +10 -10
- package/templates/project/README.md +7 -7
- package/templates/project/tsconfig.json +1 -1
package/src/cli/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// The command line. `new` starts a project; every other command loads the network from `circuit.config.ts` in the working directory, or the file `--config` names.
|
|
3
3
|
|
|
4
4
|
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
@@ -66,7 +66,15 @@ if (!existsSync(configPath)) {
|
|
|
66
66
|
const root = dirname(configPath);
|
|
67
67
|
useRoot(root);
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/** The config, a TypeScript file. Bun imports it as it is; Node needs jiti, which also resolves imports written without an extension. */
|
|
70
|
+
async function load(path: string): Promise<Network> {
|
|
71
|
+
if (process.versions.bun) return (await import(path)).default;
|
|
72
|
+
|
|
73
|
+
const { createJiti } = await import("jiti");
|
|
74
|
+
return createJiti(import.meta.url).import(path, { default: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const network = await load(configPath);
|
|
70
78
|
|
|
71
79
|
const flags = rest.filter((arg) => arg.startsWith("--"));
|
|
72
80
|
const names = rest.filter((arg) => !arg.startsWith("--"));
|
package/src/cli/new.ts
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
// `circuit new`: start a project from one of the patterns in templates/. In a terminal it asks what it needs; the same answers can be given as options, for a script or an AI agent.
|
|
2
2
|
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
6
|
import { createInterface } from "node:readline/promises";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/** Circuit's own directory: this file runs from src/cli/ in Bun, and from dist/ once built. */
|
|
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;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return directory;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ROOT = packageRoot();
|
|
23
|
+
const TEMPLATES = join(ROOT, "templates");
|
|
24
|
+
const circuit = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
|
|
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
|
+
}
|
|
10
42
|
|
|
11
43
|
const PATTERNS = {
|
|
12
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 },
|
|
@@ -28,7 +60,8 @@ const USAGE = `usage: circuit new [directory] [options]
|
|
|
28
60
|
--pattern <name> single-site, edge-router or colocation
|
|
29
61
|
--asn <number> your AS number, for edge-router and colocation
|
|
30
62
|
--1password write .env with 1Password references instead of empty values
|
|
31
|
-
--
|
|
63
|
+
--use <manager> npm, pnpm or bun; the one that ran this by default
|
|
64
|
+
--no-install do not install the dependencies`;
|
|
32
65
|
|
|
33
66
|
function fail(message: string): never {
|
|
34
67
|
console.error(message);
|
|
@@ -55,7 +88,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
55
88
|
return;
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
const valued = new Set(["--pattern", "--asn"]);
|
|
91
|
+
const valued = new Set(["--pattern", "--asn", "--use"]);
|
|
59
92
|
const positional = args.filter((arg, index) => !arg.startsWith("--") && !valued.has(args[index - 1] ?? ""));
|
|
60
93
|
|
|
61
94
|
const interactive = process.stdin.isTTY === true;
|
|
@@ -89,6 +122,10 @@ export async function create(args: string[]): Promise<void> {
|
|
|
89
122
|
}
|
|
90
123
|
if (asn && !/^\d+$/.test(asn)) fail(`${asn} is not an AS number`);
|
|
91
124
|
|
|
125
|
+
const manager = (option(args, "use") ?? detectManager()) as Manager;
|
|
126
|
+
if (!(manager in MANAGERS)) fail(`no package manager ${manager}; there are ${Object.keys(MANAGERS).join(", ")}`);
|
|
127
|
+
const { install, run, types } = MANAGERS[manager];
|
|
128
|
+
|
|
92
129
|
const onePassword =
|
|
93
130
|
args.includes("--1password") || (await ask("Keep the secrets in 1Password? y or n", "n")).toLowerCase().startsWith("y");
|
|
94
131
|
terminal?.close();
|
|
@@ -98,6 +135,16 @@ export async function create(args: string[]): Promise<void> {
|
|
|
98
135
|
cpSync(join(TEMPLATES, "project"), target, { recursive: true });
|
|
99
136
|
renameSync(join(target, "gitignore"), join(target, ".gitignore"));
|
|
100
137
|
|
|
138
|
+
// What the templates say about npm, said for the package manager in use.
|
|
139
|
+
for (const file of ["AGENTS.md", "README.md", "tsconfig.json"]) {
|
|
140
|
+
const path = join(target, file);
|
|
141
|
+
const text = readFileSync(path, "utf8")
|
|
142
|
+
.replaceAll("npx circuit", run)
|
|
143
|
+
.replaceAll("npm install", install)
|
|
144
|
+
.replaceAll('"types": ["node"]', `"types": ["${types}"]`);
|
|
145
|
+
writeFileSync(path, text);
|
|
146
|
+
}
|
|
147
|
+
|
|
101
148
|
const sources = filesIn(target).filter((file) => file.endsWith(".ts"));
|
|
102
149
|
const secrets = new Set<string>();
|
|
103
150
|
|
|
@@ -127,24 +174,25 @@ export async function create(args: string[]): Promise<void> {
|
|
|
127
174
|
private: true,
|
|
128
175
|
type: "module",
|
|
129
176
|
dependencies: { "@takodotid/circuit": circuit.version },
|
|
130
|
-
devDependencies: {
|
|
177
|
+
devDependencies: { [`@types/${types}`]: circuit.devDependencies[`@types/${types}`] },
|
|
131
178
|
};
|
|
132
179
|
writeFileSync(join(target, "package.json"), JSON.stringify(packageJson, null, 4) + "\n");
|
|
133
180
|
|
|
134
181
|
if (!args.includes("--no-install")) {
|
|
135
|
-
|
|
136
|
-
|
|
182
|
+
// npm and pnpm are scripts on Windows, which only a shell runs.
|
|
183
|
+
const result = spawnSync(install, { cwd: target, stdio: "inherit", shell: true });
|
|
184
|
+
if (result.status !== 0) console.log(`${install} failed; run it again in the project.`);
|
|
137
185
|
}
|
|
138
186
|
|
|
139
187
|
const steps = [
|
|
140
188
|
`cd ${directory}`,
|
|
141
189
|
onePassword
|
|
142
|
-
?
|
|
143
|
-
:
|
|
190
|
+
? `Point each line of .env at the item that holds it in 1Password, then run: ${run} secrets`
|
|
191
|
+
: `Fill in each password in .env, then run: ${run} secrets`,
|
|
144
192
|
"Change the names, addresses and ports in the device files to your own.",
|
|
145
193
|
...(PATTERNS[pattern].asn && (!asn || asn === EXAMPLE_ASN) ? ["Put your own AS number in routing.ts."] : []),
|
|
146
|
-
|
|
147
|
-
|
|
194
|
+
`${run} validate`,
|
|
195
|
+
`${run} snapshot, then ${run} diff, to see what would change on each device`,
|
|
148
196
|
];
|
|
149
197
|
|
|
150
198
|
console.log(`\nCreated ${directory} from the pattern "${PATTERNS[pattern].title}".\n\nNext:`);
|
package/src/core/secrets.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Secrets are references in the config and values only at the moment a command is sent. A reference names a variable, set in the environment or in `.env` beside the config, or a file kept out of the repository. A variable's value may itself be a 1Password reference, `op://vault/item/field`, read with the 1Password CLI.
|
|
2
2
|
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import type { Secret } from "../schema";
|
|
@@ -44,19 +45,16 @@ function readOnePassword(name: string, reference: string): string {
|
|
|
44
45
|
const cached = fromOnePassword.get(reference);
|
|
45
46
|
if (cached !== undefined) return cached;
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} catch {
|
|
51
|
-
throw new Error(`secret ${name} is a 1Password reference, and the 1Password CLI, op, is not installed`);
|
|
52
|
-
}
|
|
48
|
+
const result = spawnSync("op", ["read", "--no-newline", reference], { encoding: "utf8" });
|
|
49
|
+
|
|
50
|
+
if (result.error) throw new Error(`secret ${name} is a 1Password reference, and the 1Password CLI, op, is not installed`);
|
|
53
51
|
|
|
54
|
-
if (result.
|
|
55
|
-
const reason = result.stderr
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
const reason = result.stderr.trim() || `op exited with ${result.status}`;
|
|
56
54
|
throw new Error(`secret ${name}: 1Password could not read ${reference}: ${reason}`);
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
const value = result.stdout
|
|
57
|
+
const value = result.stdout;
|
|
60
58
|
fromOnePassword.set(reference, value);
|
|
61
59
|
return value;
|
|
62
60
|
}
|
|
@@ -18,21 +18,21 @@ What each field means is written as a comment on the field, in `node_modules/@ta
|
|
|
18
18
|
|
|
19
19
|
| Command | What it does | Changes a device |
|
|
20
20
|
| -------------------------------------- | -------------------------------------------------------------------- | ---------------- |
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
21
|
+
| `npx circuit validate` | Checks every file and every rule in the checks | no |
|
|
22
|
+
| `npx circuit diff [device]` | Shows what would change, compared with the last snapshot | no |
|
|
23
|
+
| `npx circuit build <device>` | Prints the full configuration the device should run | no |
|
|
24
|
+
| `npx circuit snapshot [device]` | Reads what each device runs now into `.circuit/state/` | no, only reads |
|
|
25
|
+
| `npx circuit apply <device>` | Reads the device, then shows what would change on it | no |
|
|
26
|
+
| `npx circuit apply <device> --confirm` | Sends the change to the device | **yes** |
|
|
27
|
+
| `npx circuit secrets` | Lists every secret the config uses, and whether each one can be read | no |
|
|
28
28
|
|
|
29
29
|
## Making a change
|
|
30
30
|
|
|
31
31
|
1. Edit the device's file.
|
|
32
|
-
2. Run `
|
|
33
|
-
3. Run `
|
|
32
|
+
2. Run `npx circuit validate`. Fix every error.
|
|
33
|
+
3. Run `npx circuit diff <device>` and read the result.
|
|
34
34
|
4. Show the owner what will change and why, in plain words.
|
|
35
|
-
5. Run `
|
|
35
|
+
5. Run `npx circuit apply <device>` to see the plan against the live device.
|
|
36
36
|
6. **Stop.** Only run `apply <device> --confirm` after the owner says yes to that exact plan. Never confirm on your own, even if you were told to finish the task.
|
|
37
37
|
7. After the apply, commit the device file together with the updated `.circuit/state/`.
|
|
38
38
|
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
This repository describes the network with [Circuit](https://circuit.tako.id). Each device has a file; Circuit makes the device match it.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
npm install # once
|
|
7
|
+
npx circuit secrets # check every secret can be read
|
|
8
|
+
npx circuit validate # check the files
|
|
9
|
+
npx circuit snapshot # read what each device runs now
|
|
10
|
+
npx circuit diff # see what would change
|
|
11
|
+
npx circuit apply <device> # see the plan against the device
|
|
12
|
+
npx circuit apply <device> --confirm
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Secrets go in `.env`, which is never committed. `.circuit/` is written by Circuit; commit it, but never edit it.
|