@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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// Which Circuit is current, and getting it. The installed `circuit` is the manager: before every command but `uninstall`, it asks the npm registry, at most every 30 seconds, and updates itself to the newest release of its major version: `latest` on npm, or `rc` while it runs a release candidate. A project's own version changes only when the person picks one (`circuit update`). A version a security advisory affects (see `Advisories`) never runs. Every binary Circuit downloads is checked against its release's SHA256SUMS, signed with the release key (see `Signing`).
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { type Advisory, Advisories } from "./advisories";
|
|
9
|
+
import { Signing } from "./signing";
|
|
10
|
+
import { Ui } from "./ui";
|
|
11
|
+
import { VERSION } from "./version";
|
|
12
|
+
|
|
13
|
+
/** One published version, and why it is deprecated, when it is. */
|
|
14
|
+
export type Release = { version: string; deprecated?: string };
|
|
15
|
+
|
|
16
|
+
/** What Circuit learns from the registry. */
|
|
17
|
+
export type Latest = {
|
|
18
|
+
/** The npm dist-tags, such as `{ latest: "1.0.0", rc: "1.1.0-rc.1", "latest-1.9": "1.9.5" }`. */
|
|
19
|
+
tags: Record<string, string>;
|
|
20
|
+
/** Why this version is deprecated, when it is. */
|
|
21
|
+
deprecated?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class Releases {
|
|
25
|
+
static readonly REGISTRY = "https://registry.npmjs.org";
|
|
26
|
+
static readonly PACKAGE = "@takodotid/circuit";
|
|
27
|
+
static readonly DOWNLOADS = "https://github.com/takodotid/circuit/releases/download";
|
|
28
|
+
|
|
29
|
+
/** How long a check counts before Circuit asks again. */
|
|
30
|
+
private static readonly FRESH_SECONDS = 30;
|
|
31
|
+
/** How long Circuit waits for the registry before going on without it. */
|
|
32
|
+
private static readonly TIMEOUT_MS = 3000;
|
|
33
|
+
/** Set for the Circuit a manager started after updating itself, so it doesn't update again. */
|
|
34
|
+
private static readonly UPDATED = "CIRCUIT_UPDATED";
|
|
35
|
+
|
|
36
|
+
/** How many versions `circuit update` offers, newest first. */
|
|
37
|
+
static readonly OFFERED = 5;
|
|
38
|
+
|
|
39
|
+
/** Where Circuit keeps what it learned and the versions projects pin: `~/.circuit`, or `CIRCUIT_HOME` for a test. */
|
|
40
|
+
static home(): string {
|
|
41
|
+
return process.env.CIRCUIT_HOME || join(homedir(), ".circuit");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private static cachePath(): string {
|
|
45
|
+
return join(Releases.home(), "latest.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Below 0 when `a` is older than `b`, 0 when the same, above 0 when newer. Versions as semver writes them, such as `1.0.0-rc.2`. */
|
|
49
|
+
static compare(a: string, b: string): number {
|
|
50
|
+
const parse = (version: string) => {
|
|
51
|
+
const [core, pre] = version.replace(/^v/, "").split("-", 2) as [string, string | undefined];
|
|
52
|
+
return { core: core.split(".").map(Number), pre: pre?.split(".") ?? [] };
|
|
53
|
+
};
|
|
54
|
+
const left = parse(a);
|
|
55
|
+
const right = parse(b);
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < 3; i++) {
|
|
58
|
+
const difference = (left.core[i] ?? 0) - (right.core[i] ?? 0);
|
|
59
|
+
if (difference) return difference;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A release candidate comes before its release.
|
|
63
|
+
if (!left.pre.length || !right.pre.length) return right.pre.length - left.pre.length;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i++) {
|
|
66
|
+
const x = left.pre[i];
|
|
67
|
+
const y = right.pre[i];
|
|
68
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1;
|
|
69
|
+
if (x === y) continue;
|
|
70
|
+
|
|
71
|
+
const numbers = /^\d+$/.test(x) && /^\d+$/.test(y);
|
|
72
|
+
return numbers ? Number(x) - Number(y) : x < y ? -1 : 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static major(version: string): number {
|
|
79
|
+
return Number(version.replace(/^v/, "").split(".")[0]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The version this Circuit should be at: the newest stable one of its major version, or the newest release candidate while it runs one. Undefined when the registry names none. */
|
|
83
|
+
static target(latest: Latest, current = VERSION): string | undefined {
|
|
84
|
+
const candidates = [latest.tags.latest, current.includes("-") ? latest.tags.rc : undefined].filter(
|
|
85
|
+
(version): version is string => version !== undefined && Releases.major(version) === Releases.major(current)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return candidates.sort((a, b) => Releases.compare(b, a))[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A newer major version than this one, when the registry's stable release is one. */
|
|
92
|
+
static newerMajor(latest: Latest, current = VERSION): string | undefined {
|
|
93
|
+
const stable = latest.tags.latest;
|
|
94
|
+
return stable && Releases.major(stable) > Releases.major(current) ? stable : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static readCache(): { checked: number; version: string; latest: Latest } | undefined {
|
|
98
|
+
try {
|
|
99
|
+
const cached = JSON.parse(readFileSync(Releases.cachePath(), "utf8"));
|
|
100
|
+
return cached.version === VERSION ? cached : undefined;
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private static async json(url: string): Promise<Record<string, unknown>> {
|
|
107
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(Releases.TIMEOUT_MS), headers: { accept: "application/json" } });
|
|
108
|
+
if (!response.ok) throw new Error(`the registry answered ${response.status}`);
|
|
109
|
+
return (await response.json()) as Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The registry's dist-tags, and whether this version is deprecated, from the cache when read in the last 30 seconds. `offline` when the registry could not be read, with the last answer, if any. */
|
|
113
|
+
static async latest(fresh = false): Promise<{ latest?: Latest; offline: boolean }> {
|
|
114
|
+
const cached = Releases.readCache();
|
|
115
|
+
if (!fresh && cached && Date.now() - cached.checked < Releases.FRESH_SECONDS * 1000)
|
|
116
|
+
return { latest: cached.latest, offline: false };
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const [tags, own] = await Promise.all([
|
|
120
|
+
Releases.json(`${Releases.REGISTRY}/-/package/${Releases.PACKAGE}/dist-tags`),
|
|
121
|
+
// A version not published, such as one built from the sources, is not deprecated.
|
|
122
|
+
Releases.json(`${Releases.REGISTRY}/${Releases.PACKAGE}/${VERSION}`).catch(() => ({}) as Record<string, unknown>),
|
|
123
|
+
]);
|
|
124
|
+
const latest: Latest = {
|
|
125
|
+
tags: tags as Record<string, string>,
|
|
126
|
+
...(typeof own.deprecated === "string" ? { deprecated: own.deprecated } : {}),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(Releases.home(), { recursive: true });
|
|
131
|
+
writeFileSync(Releases.cachePath(), JSON.stringify({ checked: Date.now(), version: VERSION, latest }));
|
|
132
|
+
} catch {}
|
|
133
|
+
return { latest, offline: false };
|
|
134
|
+
} catch {
|
|
135
|
+
return { ...(cached ? { latest: cached.latest } : {}), offline: true };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The security advisories, from the cache when read in the last two minutes. */
|
|
140
|
+
static advisories(fresh = false): Promise<{ advisories: Advisory[]; offline: boolean }> {
|
|
141
|
+
return Advisories.read(Releases.home(), fresh);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The advisories that affect `version`. */
|
|
145
|
+
static affecting(advisories: readonly Advisory[], version: string): Advisory[] {
|
|
146
|
+
return Advisories.affecting(advisories, version, Releases.compare);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Before every command but `uninstall`: the manager updates itself to the newest release of its major and runs the command again
|
|
151
|
+
* as that version; it warns when it could not check, and when this version is deprecated. Run from the sources, or as a version a
|
|
152
|
+
* manager started, it only checks.
|
|
153
|
+
*/
|
|
154
|
+
static async check(argv: readonly string[]): Promise<void> {
|
|
155
|
+
const [{ latest, offline }, advisories] = await Ui.wait("Checking for a newer Circuit", () =>
|
|
156
|
+
Promise.all([Releases.latest(), Releases.advisories()])
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (offline || advisories.offline)
|
|
160
|
+
Ui.warn("Could not reach the npm registry or GitHub, so Circuit could not check that it is current and safe. Go carefully.");
|
|
161
|
+
if (latest?.deprecated) Ui.warn(`Circuit ${VERSION} is deprecated: ${latest.deprecated}`);
|
|
162
|
+
|
|
163
|
+
const major = latest && Releases.newerMajor(latest);
|
|
164
|
+
if (major)
|
|
165
|
+
Ui.warn(
|
|
166
|
+
`Circuit ${major} is out, a new major version. Circuit stays on ${Releases.major(VERSION)}.x; read what changed at https://circuit.tako.id before you move.`
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const target = latest && Releases.target(latest);
|
|
170
|
+
if (!target || Releases.compare(VERSION, target) >= 0 || Releases.affecting(advisories.advisories, target).length) return;
|
|
171
|
+
|
|
172
|
+
const binary = Releases.binary();
|
|
173
|
+
if (!binary || process.env[Releases.UPDATED]) {
|
|
174
|
+
Ui.warn(`Circuit ${target} is out, and this is ${VERSION}.`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
Releases.replace(binary, await Ui.step(`Updating Circuit to ${target}`, () => Releases.fetchBinary(target)));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
Ui.warn(`Circuit ${target} is out, and could not be installed: ${(error as Error).message}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
Ui.done(`Circuit is ${target} now. Your projects keep the version they pin.`);
|
|
185
|
+
|
|
186
|
+
// The command runs on as the new version.
|
|
187
|
+
const result = spawnSync(binary, argv, { stdio: "inherit", env: { ...process.env, [Releases.UPDATED]: "1" } });
|
|
188
|
+
if (result.error) Ui.fail(`Circuit ${target} could not start: ${result.error.message}`);
|
|
189
|
+
process.exit(result.status ?? 1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** This Circuit's own file, when it runs as the installed binary rather than from its sources. */
|
|
193
|
+
static binary(): string | undefined {
|
|
194
|
+
return /^circuit(-[\w-]+)?(\.exe)?$/i.test(basename(process.execPath)) ? process.execPath : undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The release file for this system, such as `circuit-linux-x64`. Undefined on a system Circuit is not built for. */
|
|
198
|
+
static asset(platform: string = process.platform, arch: string = process.arch): string | undefined {
|
|
199
|
+
const systems: Record<string, string> = { linux: "linux", darwin: "darwin", win32: "windows" };
|
|
200
|
+
const system = systems[platform];
|
|
201
|
+
if (!system || !["x64", "arm64"].includes(arch) || (system === "windows" && arch !== "x64")) return undefined;
|
|
202
|
+
|
|
203
|
+
return `circuit-${system}-${arch}${system === "windows" ? ".exe" : ""}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Remove what an update on Windows left: the old binary, kept aside while it still ran. */
|
|
207
|
+
static tidy(): void {
|
|
208
|
+
const binary = Releases.binary();
|
|
209
|
+
if (!binary) return;
|
|
210
|
+
|
|
211
|
+
const folder = dirname(binary);
|
|
212
|
+
for (const name of readdirSync(folder).filter((file) => file.startsWith(`${basename(binary)}.old`))) {
|
|
213
|
+
// One still running in another terminal is locked on Windows: it goes on a later run.
|
|
214
|
+
try {
|
|
215
|
+
rmSync(join(folder, name), { force: true });
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The file of a release's SHA256SUMS that names `asset`, when the signature is the release key's. Throws otherwise. */
|
|
221
|
+
static expectedHash(sums: Buffer, signature: string, asset: string, keys: readonly string[] = Signing.KEYS): string {
|
|
222
|
+
if (!Signing.verify(sums, signature, keys))
|
|
223
|
+
throw new Error("the release's SHA256SUMS is not signed with Circuit's release key, so nothing was installed");
|
|
224
|
+
|
|
225
|
+
const expected = sums
|
|
226
|
+
.toString("utf8")
|
|
227
|
+
.split("\n")
|
|
228
|
+
.find((line) => line.trim().endsWith(` ${asset}`) || line.trim().endsWith(`*${asset}`))
|
|
229
|
+
?.split(/\s+/)[0];
|
|
230
|
+
if (!expected) throw new Error(`the release's SHA256SUMS names no ${asset}`);
|
|
231
|
+
return expected;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** One release's binary for this system, checked against the release's SHA256SUMS and its signature. */
|
|
235
|
+
static async fetchBinary(version: string): Promise<Buffer> {
|
|
236
|
+
const asset = Releases.asset() ?? Ui.fail(`Circuit is not built for ${process.platform} ${process.arch}.`);
|
|
237
|
+
const download = async (file: string) => {
|
|
238
|
+
const response = await fetch(`${Releases.DOWNLOADS}/v${version}/${file}`);
|
|
239
|
+
if (!response.ok) throw new Error(`${file} of Circuit ${version}: GitHub answered ${response.status}`);
|
|
240
|
+
return Buffer.from(await response.arrayBuffer());
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const [file, sums, signature] = await Promise.all([download(asset), download("SHA256SUMS"), download("SHA256SUMS.sig")]);
|
|
244
|
+
const expected = Releases.expectedHash(sums, signature.toString("utf8"), asset);
|
|
245
|
+
const actual = createHash("sha256").update(file).digest("hex");
|
|
246
|
+
if (expected !== actual) throw new Error(`${asset} does not match the release's signed SHA256SUMS, so it was not installed`);
|
|
247
|
+
|
|
248
|
+
return file;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Every published version, and which are deprecated. A stop when the registry can't be read. */
|
|
252
|
+
static async releases(): Promise<Release[]> {
|
|
253
|
+
try {
|
|
254
|
+
const packument = await Ui.step("Checking for a newer Circuit", async () => {
|
|
255
|
+
const response = await fetch(`${Releases.REGISTRY}/${Releases.PACKAGE}`, {
|
|
256
|
+
signal: AbortSignal.timeout(Releases.TIMEOUT_MS * 3),
|
|
257
|
+
// The short form npm itself installs from: each version and whether it is deprecated.
|
|
258
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok) throw new Error(`the registry answered ${response.status}`);
|
|
261
|
+
return (await response.json()) as { versions?: Record<string, { deprecated?: string }> };
|
|
262
|
+
});
|
|
263
|
+
return Object.entries(packument.versions ?? {}).map(([version, about]) => ({
|
|
264
|
+
version,
|
|
265
|
+
...(about.deprecated ? { deprecated: about.deprecated } : {}),
|
|
266
|
+
}));
|
|
267
|
+
} catch {
|
|
268
|
+
return Ui.fail("Could not reach the npm registry to learn the newest Circuit. Check the internet connection and try again.");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The versions to offer after `current`, newest first, at most `count`: of its major version, never one an advisory affects. A
|
|
274
|
+
* release candidate only while `current` is one. The safe version closest to `current`, the fix that changes the least, is always
|
|
275
|
+
* among them.
|
|
276
|
+
*/
|
|
277
|
+
static choices(
|
|
278
|
+
releases: readonly Release[],
|
|
279
|
+
current: string,
|
|
280
|
+
advisories: readonly Advisory[] = [],
|
|
281
|
+
count = Releases.OFFERED
|
|
282
|
+
): Release[] {
|
|
283
|
+
const safe = releases
|
|
284
|
+
.filter(
|
|
285
|
+
(release) =>
|
|
286
|
+
Releases.major(release.version) === Releases.major(current) &&
|
|
287
|
+
Releases.compare(release.version, current) > 0 &&
|
|
288
|
+
!Releases.affecting(advisories, release.version).length &&
|
|
289
|
+
(current.includes("-") || !release.version.includes("-"))
|
|
290
|
+
)
|
|
291
|
+
.sort((a, b) => Releases.compare(b.version, a.version));
|
|
292
|
+
|
|
293
|
+
const offered = safe.slice(0, count);
|
|
294
|
+
const closest = safe.at(-1);
|
|
295
|
+
if (closest && !offered.includes(closest)) offered[offered.length - 1] = closest;
|
|
296
|
+
return offered;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Which version `what` moves to: the person picks from the newest few, the newest at the top, or stays, unless an advisory affects
|
|
301
|
+
* the version it runs: then it must move. With `confirmed`, the newest. Undefined when it stays.
|
|
302
|
+
*/
|
|
303
|
+
static async pick(current: string, confirmed: boolean, what: string): Promise<string | undefined> {
|
|
304
|
+
const [releases, { advisories }] = await Promise.all([Releases.releases(), Releases.advisories(true)]);
|
|
305
|
+
const choices = Releases.choices(releases, current, advisories);
|
|
306
|
+
const affected = Releases.affecting(advisories, current);
|
|
307
|
+
const closest = choices.at(-1)?.version;
|
|
308
|
+
|
|
309
|
+
if (!choices.length) {
|
|
310
|
+
if (affected.length)
|
|
311
|
+
Ui.fail(
|
|
312
|
+
`${what} runs Circuit ${current}, which ${affected[0]!.id} affects, and no fixed version of ${Releases.major(current)}.x is out yet. See ${affected[0]!.url}`
|
|
313
|
+
);
|
|
314
|
+
Ui.done(`${what} runs Circuit ${current}, the newest of ${Releases.major(current)}.x.`);
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
if (confirmed) return choices[0]!.version;
|
|
318
|
+
if (!Ui.interactive) Ui.fail("Not updated. Without a person to answer, add --confirm, which takes the newest.");
|
|
319
|
+
|
|
320
|
+
const problem = affected.map((advisory) => `${advisory.summary} (${advisory.severity}): ${advisory.url}`).join("\n");
|
|
321
|
+
Ui.note(
|
|
322
|
+
`${what} runs Circuit ${current}.${affected.length ? `\nIt has a security problem, so it must move to a fixed version:\n${problem}` : ""}\nWhat changed: https://github.com/takodotid/circuit/releases\nRun circuit validate and circuit diff after, before you apply.`,
|
|
323
|
+
affected.length ? "Update required" : "Update"
|
|
324
|
+
);
|
|
325
|
+
const picked = await Ui.select<string>(affected.length ? "Move to which fixed version?" : "Which version?", [
|
|
326
|
+
...choices.map((release, i) => ({
|
|
327
|
+
value: release.version,
|
|
328
|
+
label: release.version,
|
|
329
|
+
hint: [
|
|
330
|
+
i === 0 ? "newest" : "",
|
|
331
|
+
release.version === closest && choices.length > 1 ? "closest to yours, the least that changes" : "",
|
|
332
|
+
release.version.includes("-") ? "release candidate" : "",
|
|
333
|
+
release.deprecated ? `deprecated: ${release.deprecated}` : "",
|
|
334
|
+
]
|
|
335
|
+
.filter(Boolean)
|
|
336
|
+
.join(", "),
|
|
337
|
+
})),
|
|
338
|
+
...(affected.length ? [] : [{ value: "", label: `Stay on ${current}` }]),
|
|
339
|
+
]);
|
|
340
|
+
return picked || undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Update the installed manager to a version the person picks. Gives back that version. */
|
|
344
|
+
static async updateApp(confirmed: boolean): Promise<string | undefined> {
|
|
345
|
+
const binary =
|
|
346
|
+
Releases.binary() ?? Ui.fail("This Circuit runs from its sources. Update it where it came from, such as with git pull.");
|
|
347
|
+
const target = await Releases.pick(VERSION, confirmed, "The app");
|
|
348
|
+
if (!target) return undefined;
|
|
349
|
+
|
|
350
|
+
Releases.replace(binary, await Ui.step(`Downloading Circuit ${target}`, () => Releases.fetchBinary(target)));
|
|
351
|
+
Ui.done(`Circuit is ${target} now.`);
|
|
352
|
+
return target;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Put `bytes` in place of the running binary. Windows does not let a running program be overwritten, but lets it be renamed, so the old one is moved aside and removed on the next run. */
|
|
356
|
+
static replace(binary: string, bytes: Buffer): void {
|
|
357
|
+
const next = `${binary}.new`;
|
|
358
|
+
writeFileSync(next, bytes);
|
|
359
|
+
|
|
360
|
+
if (process.platform === "win32") {
|
|
361
|
+
// An old one still running elsewhere can't be removed: the new one goes in beside it, under another name.
|
|
362
|
+
try {
|
|
363
|
+
rmSync(`${binary}.old`, { force: true });
|
|
364
|
+
} catch {}
|
|
365
|
+
if (existsSync(`${binary}.old`)) renameSync(`${binary}.old`, `${binary}.old-${Date.now()}`);
|
|
366
|
+
renameSync(binary, `${binary}.old`);
|
|
367
|
+
renameSync(next, binary);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
chmodSync(next, 0o755);
|
|
372
|
+
renameSync(next, binary);
|
|
373
|
+
}
|
|
374
|
+
}
|