@takodotid/circuit 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/index-n4zpd25n.js +936 -0
  3. package/dist/chunks/{index-dczpxzzm.js → index-v8zgr7wm.js} +33 -14
  4. package/dist/chunks/{new-7n8qvtjd.js → new-ny89s7y7.js} +4 -6
  5. package/dist/chunks/server-zapfzhv4.js +244 -0
  6. package/dist/cli/index.js +855 -803
  7. package/dist/index.js +20 -21
  8. package/dist/presets/index.js +20 -21
  9. package/dist/types/adapters/devices/derive.d.ts +2 -0
  10. package/dist/types/core/across.d.ts +4 -0
  11. package/dist/types/core/addr.d.ts +10 -0
  12. package/dist/types/core/addresses.d.ts +46 -0
  13. package/dist/types/core/files.d.ts +6 -0
  14. package/dist/types/core/graph.d.ts +31 -1
  15. package/dist/types/core/schema-docs.d.ts +16 -0
  16. package/package.json +7 -7
  17. package/src/adapters/devices/derive.ts +32 -0
  18. package/src/adapters/devices/raisecom-ros/index.ts +23 -3
  19. package/src/adapters/devices/routeros/index.ts +10 -0
  20. package/src/adapters/devices/vrp/index.ts +10 -1
  21. package/src/cli/graph.ts +1 -15
  22. package/src/cli/index.ts +154 -20
  23. package/src/cli/rename.ts +129 -0
  24. package/src/core/across.ts +322 -0
  25. package/src/core/addr.ts +29 -0
  26. package/src/core/addresses.ts +316 -0
  27. package/src/core/files.ts +32 -0
  28. package/src/core/graph.ts +27 -6
  29. package/src/core/schema-docs.ts +166 -0
  30. package/src/core/validate.ts +2 -52
  31. package/src/studio/data.ts +255 -0
  32. package/src/studio/git.ts +119 -0
  33. package/src/studio/public/app.js +165 -0
  34. package/src/studio/public/compare.js +243 -0
  35. package/src/studio/public/diff.js +97 -0
  36. package/src/studio/public/dom.js +108 -0
  37. package/src/studio/public/evaluate.js +648 -0
  38. package/src/studio/public/export.js +122 -0
  39. package/src/studio/public/favicon.svg +12 -0
  40. package/src/studio/public/graph.js +983 -0
  41. package/src/studio/public/index.html +71 -0
  42. package/src/studio/public/net.js +128 -0
  43. package/src/studio/public/special-gothic-latin-ext.woff2 +0 -0
  44. package/src/studio/public/special-gothic-latin.woff2 +0 -0
  45. package/src/studio/public/special-gothic-license.txt +93 -0
  46. package/src/studio/public/style.css +1334 -0
  47. package/src/studio/public/tables.js +1223 -0
  48. package/src/studio/public/tools.js +578 -0
  49. package/src/studio/server.ts +219 -0
  50. package/templates/project/AGENTS.md +2 -0
  51. package/dist/chunks/index-fq57hya4.js +0 -20
  52. package/dist/chunks/studio-femhydvd.js +0 -764
  53. package/src/cli/studio-page.ts +0 -876
  54. package/src/cli/studio.ts +0 -126
@@ -1,11 +1,11 @@
1
1
  // src/core/addr.ts
2
- var familyOf = (address) => address.includes(":") ? "ipv6" : "ipv4";
2
+ var familyOf2 = (address) => address.includes(":") ? "ipv6" : "ipv4";
3
3
  var bitsOf = (family) => family === "ipv4" ? 32 : 128;
4
4
  function parse(address) {
5
5
  if (!address.includes(":")) {
6
6
  const octets = address.split(".");
7
- const valid2 = octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
8
- if (!valid2)
7
+ const valid = octets.length === 4 && octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255);
8
+ if (!valid)
9
9
  return null;
10
10
  return octets.reduce((value, octet) => value << 8n | BigInt(octet), 0n);
11
11
  }
@@ -24,15 +24,15 @@ function parse(address) {
24
24
  }
25
25
  function split(cidr) {
26
26
  const [address, lengthText] = cidr.split("/");
27
- const family = familyOf(address);
27
+ const family = familyOf2(address);
28
28
  const length = Number(lengthText);
29
29
  const valid = parse(address) !== null && lengthText !== undefined && Number.isInteger(length) && length >= 0 && length <= bitsOf(family);
30
30
  if (!valid)
31
31
  throw new Error(`'${cidr}' is not an address with a length`);
32
32
  return { addr: address, len: length, family };
33
33
  }
34
- var isAddress = (text) => parse(text) !== null;
35
- function isCidr(text) {
34
+ var isAddress2 = (text) => parse(text) !== null;
35
+ function isCidr2(text) {
36
36
  try {
37
37
  split(text);
38
38
  return true;
@@ -73,10 +73,10 @@ function network(cidr) {
73
73
  const { addr, len, family } = split(cidr);
74
74
  return `${format(parse(addr) & mask(len, family), family)}/${len}`;
75
75
  }
76
- function contains(outer, inner) {
76
+ function contains2(outer, inner) {
77
77
  const outerParts = split(outer);
78
78
  const [innerAddress, innerLengthText] = inner.split("/");
79
- if (familyOf(innerAddress) !== outerParts.family)
79
+ if (familyOf2(innerAddress) !== outerParts.family)
80
80
  return false;
81
81
  const innerLength = innerLengthText === undefined ? bitsOf(outerParts.family) : Number(innerLengthText);
82
82
  if (innerLength < outerParts.len)
@@ -84,9 +84,28 @@ function contains(outer, inner) {
84
84
  const outerMask = mask(outerParts.len, outerParts.family);
85
85
  return (parse(innerAddress) & outerMask) === (parse(outerParts.addr) & outerMask);
86
86
  }
87
+ var formatAddress = (value, family) => format(value, family);
88
+ function rangeOf(cidr) {
89
+ const { addr, len, family } = split(cidr);
90
+ const hostBits = mask(len, family) ^ (1n << BigInt(bitsOf(family))) - 1n;
91
+ const first = parse(addr) & mask(len, family);
92
+ return { first, last: first | hostBits, family };
93
+ }
94
+ function prefixesBetween(first, last, family) {
95
+ const bits = bitsOf(family);
96
+ const prefixes = [];
97
+ for (let start = first;start <= last; ) {
98
+ let size = 0;
99
+ while (size < bits && start % (1n << BigInt(size + 1)) === 0n && start + (1n << BigInt(size + 1)) - 1n <= last)
100
+ size++;
101
+ prefixes.push(`${format(start, family)}/${bits - size}`);
102
+ start += 1n << BigInt(size);
103
+ }
104
+ return prefixes;
105
+ }
87
106
  var netmask = (length) => format(mask(length, "ipv4"), "ipv4");
88
107
  var wildcard = (length) => format(mask(length, "ipv4") ^ 0xffffffffn, "ipv4");
89
- var host = (cidr) => cidr.split("/")[0];
108
+ var host2 = (cidr) => cidr.split("/")[0];
90
109
  var NOT_GLOBAL_V4 = [
91
110
  "0.0.0.0/8",
92
111
  "10.0.0.0/8",
@@ -100,10 +119,10 @@ var NOT_GLOBAL_V4 = [
100
119
  "198.51.100.0/24",
101
120
  "203.0.113.0/24"
102
121
  ];
103
- function isGlobal(address) {
104
- if (familyOf(address) === "ipv6")
105
- return contains("2000::/3", address) && !contains("2001:db8::/32", address);
106
- return !NOT_GLOBAL_V4.some((prefix) => contains(prefix, address));
122
+ function isGlobal2(address) {
123
+ if (familyOf2(address) === "ipv6")
124
+ return contains2("2000::/3", address) && !contains2("2001:db8::/32", address);
125
+ return !NOT_GLOBAL_V4.some((prefix) => contains2(prefix, address));
107
126
  }
108
127
 
109
- export { familyOf, parse, split, isAddress, isCidr, network, contains, netmask, wildcard, host, isGlobal };
128
+ export { familyOf2, parse, split, isAddress2, isCidr2, network, contains2, formatAddress, rangeOf, prefixesBetween, netmask, wildcard, host2, isGlobal2 };
@@ -1,5 +1,3 @@
1
- import"./index-fq57hya4.js";
2
-
3
1
  // src/cli/new.ts
4
2
  import { spawnSync } from "node:child_process";
5
3
  import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -87,7 +85,7 @@ async function create(args) {
87
85
  if (terminal) {
88
86
  console.log(`
89
87
  Which is closest to your network?`);
90
- names.forEach((name2, index) => console.log(` ${index + 1}. ${PATTERNS[name2].title}: ${PATTERNS[name2].fits}`));
88
+ names.forEach((name, index) => console.log(` ${index + 1}. ${PATTERNS[name].title}: ${PATTERNS[name].fits}`));
91
89
  }
92
90
  pattern = names[Number(await ask("Number", "1")) - 1];
93
91
  }
@@ -120,14 +118,14 @@ Which is closest to your network?`);
120
118
  let text = readFileSync(path, "utf8");
121
119
  if (asn && asn !== EXAMPLE_ASN)
122
120
  text = text.replaceAll(new RegExp(`\\b${EXAMPLE_ASN}\\b`, "g"), asn);
123
- for (const [, name2] of text.matchAll(/secret\("([A-Z0-9_]+)"\)/g))
124
- secrets.add(name2);
121
+ for (const [, name] of text.matchAll(/secret\("([A-Z0-9_]+)"\)/g))
122
+ secrets.add(name);
125
123
  writeFileSync(path, text);
126
124
  }
127
125
  const env = [
128
126
  "# Secrets for Circuit. Never commit this file.",
129
127
  "# A value is the secret itself, or a 1Password reference, op://vault/item/field, read with the 1Password CLI.",
130
- ...[...secrets].sort().map((name2) => onePassword ? `${name2}=op://Network/${name2}/password` : `${name2}=`),
128
+ ...[...secrets].sort().map((name) => onePassword ? `${name}=op://Network/${name}/password` : `${name}=`),
131
129
  ""
132
130
  ];
133
131
  writeFileSync(join(target, ".env"), env.join(`
@@ -0,0 +1,244 @@
1
+ import {
2
+ packageRoot
3
+ } from "./index-n4zpd25n.js";
4
+
5
+ // src/studio/server.ts
6
+ import { execFile as execFile2, spawn } from "node:child_process";
7
+ import { existsSync, readFileSync, watch } from "node:fs";
8
+ import { createServer } from "node:http";
9
+ import { extname, join as join2 } from "node:path";
10
+
11
+ // src/studio/git.ts
12
+ import { execFile } from "node:child_process";
13
+ import { mkdirSync, writeFileSync } from "node:fs";
14
+ import { basename, join } from "node:path";
15
+ var environment = () => Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_")));
16
+ function git(cwd, args) {
17
+ return new Promise((done, fail) => {
18
+ execFile("git", args, { cwd, env: environment(), maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
19
+ if (error)
20
+ fail(new Error((stderr || error.message).trim()));
21
+ else
22
+ done(stdout);
23
+ });
24
+ });
25
+ }
26
+ var FORMAT = "--format=%H%x1f%h%x1f%an%x1f%aI%x1f%s";
27
+ function commitsOf(text) {
28
+ return text.split(`
29
+ `).filter(Boolean).map((line) => {
30
+ const [commit = "", short = "", author = "", date = "", subject = ""] = line.split("\x1F");
31
+ return { commit, short, author, date, subject };
32
+ });
33
+ }
34
+ async function gitTop(directory) {
35
+ try {
36
+ return (await git(directory, ["rev-parse", "--show-toplevel"])).trim();
37
+ } catch {
38
+ return;
39
+ }
40
+ }
41
+ async function history(directory, files, limit = 50) {
42
+ if (!files.length)
43
+ return [];
44
+ return commitsOf(await git(directory, ["log", `-n${limit}`, FORMAT, "--", ...files]));
45
+ }
46
+ async function references(directory) {
47
+ const names = (await git(directory, [
48
+ "for-each-ref",
49
+ "--count=100",
50
+ "--sort=-committerdate",
51
+ "--format=%(refname:short)",
52
+ "refs/heads",
53
+ "refs/remotes",
54
+ "refs/tags"
55
+ ])).split(`
56
+ `).filter((name) => name && !name.endsWith("/HEAD"));
57
+ return { names, commits: commitsOf(await git(directory, ["log", "-n30", FORMAT])) };
58
+ }
59
+ async function commitOf(directory, name) {
60
+ if (!/^[A-Za-z0-9][\w./@^~-]*$/.test(name))
61
+ return;
62
+ try {
63
+ const [commit] = commitsOf(await git(directory, ["log", "-n1", FORMAT, `${name}^{commit}`, "--"]));
64
+ return commit;
65
+ } catch {
66
+ return;
67
+ }
68
+ }
69
+ async function networkAt(configDirectory, configPath, commit, gather) {
70
+ const top = await gitTop(configDirectory);
71
+ if (!top)
72
+ throw new Error("the config is not in a git repository");
73
+ const cache = join(configDirectory, ".circuit", "cache");
74
+ mkdirSync(cache, { recursive: true });
75
+ writeFileSync(join(cache, ".gitignore"), `# Written by Circuit Studio: checkouts of other commits, removed once read.
76
+ *
77
+ `);
78
+ await git(top, ["worktree", "prune"]);
79
+ const checkout = join(cache, commit.slice(0, 12));
80
+ await git(top, ["worktree", "add", "--detach", "--force", checkout, commit]);
81
+ try {
82
+ const prefix = (await git(configDirectory, ["rev-parse", "--show-prefix"])).trim();
83
+ return await gather(join(checkout, prefix, basename(configPath)));
84
+ } finally {
85
+ await git(top, ["worktree", "remove", "--force", checkout]).catch(() => {});
86
+ }
87
+ }
88
+
89
+ // src/studio/server.ts
90
+ var isLoopback = (host) => host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
91
+ function hostAllowed(header, port, allowHosts) {
92
+ if (!header)
93
+ return false;
94
+ if (header === `127.0.0.1:${port}` || header === `localhost:${port}` || header === `[::1]:${port}`)
95
+ return true;
96
+ const name = header.replace(/:\d+$/, "").toLowerCase();
97
+ return allowHosts.some((allowed) => {
98
+ const wanted = allowed.toLowerCase();
99
+ return wanted === header.toLowerCase() || wanted === name;
100
+ });
101
+ }
102
+ var IGNORED = /(^|[\\/])(node_modules|\.git|\.circuit[\\/]cache)([\\/]|$)/;
103
+ var CONTENT_TYPES = {
104
+ ".html": "text/html; charset=utf-8",
105
+ ".css": "text/css; charset=utf-8",
106
+ ".js": "text/javascript; charset=utf-8",
107
+ ".svg": "image/svg+xml",
108
+ ".woff2": "font/woff2"
109
+ };
110
+ function gather(configPath) {
111
+ const cli = process.argv[1];
112
+ const args = [...process.execArgv, cli, "studio", "--json", `--config=${configPath}`];
113
+ return new Promise((done) => {
114
+ execFile2(process.execPath, args, { maxBuffer: 256 * 1024 * 1024 }, (error, stdout, stderr) => {
115
+ if (!error)
116
+ return done(stdout);
117
+ const message = (stderr || error.message).trim();
118
+ done(JSON.stringify({ error: message }));
119
+ });
120
+ });
121
+ }
122
+ function openBrowser(url) {
123
+ const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
124
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
125
+ child.on("error", () => {});
126
+ child.unref();
127
+ }
128
+ async function studio(options) {
129
+ const { configPath, root, port } = options;
130
+ const installed = packageRoot();
131
+ if (!installed)
132
+ throw new Error("cannot find where Circuit is installed, so Studio has no page to serve");
133
+ const pages = join2(installed, "src", "studio", "public");
134
+ let data = await gather(configPath);
135
+ const past = new Map;
136
+ const answer = async (response, work) => {
137
+ let body;
138
+ try {
139
+ const result = await work();
140
+ body = typeof result === "string" ? result : JSON.stringify(result);
141
+ } catch (error) {
142
+ body = JSON.stringify({ error: error.message });
143
+ }
144
+ response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(body);
145
+ };
146
+ const listeners = new Set;
147
+ const tell = (message) => {
148
+ for (const listener of listeners)
149
+ listener.write(`data: ${message}
150
+
151
+ `);
152
+ };
153
+ const server = createServer((request, response) => {
154
+ if (!hostAllowed(request.headers.host, port, options.allowHosts)) {
155
+ response.writeHead(403).end();
156
+ return;
157
+ }
158
+ const url = new URL(request.url ?? "/", "http://studio");
159
+ const path = url.pathname;
160
+ if (path === "/history") {
161
+ answer(response, async () => {
162
+ const name = url.searchParams.get("device") ?? "";
163
+ const files = JSON.parse(data).files?.[name];
164
+ if (!files)
165
+ throw new Error(`no device ${name}`);
166
+ return history(root, [files.source, files.snapshot].filter((file) => Boolean(file)));
167
+ });
168
+ return;
169
+ }
170
+ if (path === "/refs") {
171
+ answer(response, () => references(root));
172
+ return;
173
+ }
174
+ if (path === "/compare") {
175
+ answer(response, async () => {
176
+ const asked = url.searchParams.get("ref") ?? "";
177
+ const commit = await commitOf(root, asked);
178
+ if (!commit)
179
+ throw new Error(`${asked} names no commit here`);
180
+ if (!past.has(commit.commit))
181
+ past.set(commit.commit, networkAt(root, configPath, commit.commit, gather));
182
+ const network = await past.get(commit.commit);
183
+ if (network.startsWith('{"error"'))
184
+ past.delete(commit.commit);
185
+ return `{"commit":${JSON.stringify(commit)},"network":${network}}`;
186
+ });
187
+ return;
188
+ }
189
+ if (path === "/data") {
190
+ response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" }).end(data);
191
+ return;
192
+ }
193
+ if (path === "/events") {
194
+ response.writeHead(200, {
195
+ "content-type": "text/event-stream",
196
+ "cache-control": "no-store",
197
+ connection: "keep-alive",
198
+ "x-accel-buffering": "no"
199
+ });
200
+ response.write(`: connected
201
+
202
+ `);
203
+ listeners.add(response);
204
+ request.on("close", () => listeners.delete(response));
205
+ return;
206
+ }
207
+ const name = path === "/" ? "index.html" : path.slice(1);
208
+ const type = CONTENT_TYPES[extname(name)];
209
+ const file = join2(pages, name);
210
+ if (!/^[a-z-]+\.[a-z0-9]+$/.test(name) || !type || !existsSync(file)) {
211
+ response.writeHead(404).end();
212
+ return;
213
+ }
214
+ response.writeHead(200, { "content-type": type, "cache-control": "no-store" }).end(readFileSync(file));
215
+ });
216
+ let timer;
217
+ watch(root, { recursive: true }, (_event, filename) => {
218
+ if (filename && IGNORED.test(filename))
219
+ return;
220
+ clearTimeout(timer);
221
+ timer = setTimeout(async () => {
222
+ data = await gather(configPath);
223
+ tell("changed");
224
+ console.log(`${new Date().toLocaleTimeString()} ${filename ?? "a file"} changed, read again`);
225
+ }, 300);
226
+ });
227
+ watch(pages, () => tell("page"));
228
+ server.on("error", (error) => {
229
+ const message = error.code === "EADDRINUSE" ? `port ${port} is in use; choose another with --port=N` : error.message;
230
+ console.error(message);
231
+ process.exit(1);
232
+ });
233
+ server.listen(port, options.host, () => {
234
+ const local = isLoopback(options.host);
235
+ const url = local ? `http://127.0.0.1:${port}` : `http://${options.allowHosts[0] ?? options.host}`;
236
+ console.log(`Circuit Studio at ${url}, listening on ${options.host}:${port}, watching ${root}. Stop it with Ctrl+C.`);
237
+ if (options.open && local)
238
+ openBrowser(url);
239
+ });
240
+ }
241
+ export {
242
+ isLoopback,
243
+ studio
244
+ };