@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,69 @@
|
|
|
1
|
+
// The files a project's editor reads, in `.circuit/types/`: Circuit's functions and types, so a project's files complete and check with nothing installed.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { EDITOR_FILES } from "./editor-types.generated";
|
|
6
|
+
|
|
7
|
+
export class EditorTypes {
|
|
8
|
+
/** Where they go, in the project. */
|
|
9
|
+
static readonly FOLDER = join(".circuit", "types");
|
|
10
|
+
|
|
11
|
+
/** The project's `tsconfig.json`: Circuit writes it when it starts a project. It reads Circuit's globals, and `circuit` for `import type` in `lib/`. */
|
|
12
|
+
static readonly TSCONFIG = `${JSON.stringify(
|
|
13
|
+
{
|
|
14
|
+
compilerOptions: {
|
|
15
|
+
strict: true,
|
|
16
|
+
noEmit: true,
|
|
17
|
+
target: "ES2022",
|
|
18
|
+
module: "ESNext",
|
|
19
|
+
moduleResolution: "bundler",
|
|
20
|
+
skipLibCheck: true,
|
|
21
|
+
types: [],
|
|
22
|
+
paths: { circuit: ["./.circuit/types/config/index.ts"] },
|
|
23
|
+
},
|
|
24
|
+
include: ["**/*.ts", ".circuit/types/config/circuit.d.ts"],
|
|
25
|
+
},
|
|
26
|
+
null,
|
|
27
|
+
4
|
|
28
|
+
)}\n`;
|
|
29
|
+
|
|
30
|
+
/** A full install's `tsconfig.json`: the types come from the Circuit package in `node_modules`, so the project has no `.circuit/types`. */
|
|
31
|
+
static readonly TSCONFIG_FULL = `${JSON.stringify(
|
|
32
|
+
{
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
strict: true,
|
|
35
|
+
noEmit: true,
|
|
36
|
+
target: "ES2022",
|
|
37
|
+
module: "ESNext",
|
|
38
|
+
moduleResolution: "bundler",
|
|
39
|
+
skipLibCheck: true,
|
|
40
|
+
types: [],
|
|
41
|
+
paths: { circuit: ["./node_modules/@takodotid/circuit/src/config/index.ts"] },
|
|
42
|
+
},
|
|
43
|
+
include: ["**/*.ts", "node_modules/@takodotid/circuit/src/config/circuit.d.ts"],
|
|
44
|
+
exclude: ["node_modules/@takodotid/circuit/src/cli", "node_modules/@takodotid/circuit/src/studio"],
|
|
45
|
+
},
|
|
46
|
+
null,
|
|
47
|
+
4
|
|
48
|
+
)}\n`;
|
|
49
|
+
|
|
50
|
+
/** Writes every file again, removing the old ones first. */
|
|
51
|
+
static write(root: string): void {
|
|
52
|
+
const folder = join(root, EditorTypes.FOLDER);
|
|
53
|
+
rmSync(folder, { recursive: true, force: true });
|
|
54
|
+
|
|
55
|
+
for (const [path, text] of Object.entries(EDITOR_FILES)) {
|
|
56
|
+
mkdirSync(dirname(join(folder, path)), { recursive: true });
|
|
57
|
+
writeFileSync(join(folder, path), text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether the project's types are this Circuit's: every file as this Circuit writes it. No version is kept beside them: the project's version is `.circuit/version`, and the files themselves say whether they are current. */
|
|
62
|
+
static current(root: string): boolean {
|
|
63
|
+
const folder = join(root, EditorTypes.FOLDER);
|
|
64
|
+
return Object.entries(EDITOR_FILES).every(([path, text]) => {
|
|
65
|
+
const file = join(folder, path);
|
|
66
|
+
return existsSync(file) && readFileSync(file, "utf8") === text;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// `circuit fmt`: the project's files in its style. It is Prettier, carried inside Circuit, so nothing else is installed. The style is Prettier's own, set by the project's `.editorconfig` (indent, line length, line ends), as VS Code's Prettier extension reads it, so saving in the editor and `circuit fmt` agree. A `.prettierrc.json`, when a project adds one, goes on top, for both.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, relative, resolve } from "node:path";
|
|
5
|
+
import * as estree from "prettier/plugins/estree";
|
|
6
|
+
import * as typescript from "prettier/plugins/typescript";
|
|
7
|
+
import * as babel from "prettier/plugins/babel";
|
|
8
|
+
import type { Options } from "prettier";
|
|
9
|
+
import { format } from "prettier/standalone";
|
|
10
|
+
import { Ui } from "./ui";
|
|
11
|
+
|
|
12
|
+
export class Format {
|
|
13
|
+
/** What a new project's `.editorconfig` says: four spaces, lines up to 140, LF. */
|
|
14
|
+
static readonly EDITORCONFIG =
|
|
15
|
+
"root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\nmax_line_length = 140\n";
|
|
16
|
+
|
|
17
|
+
/** Folders that hold nothing of the project's own to format. */
|
|
18
|
+
private static readonly SKIPPED = new Set(["node_modules", ".git", ".circuit", "dist"]);
|
|
19
|
+
|
|
20
|
+
/** The settings of `.editorconfig` that apply to a TypeScript file: those under `[*]`, then under a section that names `.ts`. */
|
|
21
|
+
static editorconfig(text: string): Record<string, string> {
|
|
22
|
+
const settings: Record<string, string> = {};
|
|
23
|
+
let applies = false;
|
|
24
|
+
|
|
25
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
26
|
+
const line = raw.replace(/[#;].*$/, "").trim();
|
|
27
|
+
const section = /^\[(.+)\]$/.exec(line);
|
|
28
|
+
if (section) {
|
|
29
|
+
applies = section[1] === "*" || /\bts\b/.test(section[1]!);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [key, value] = line.split("=").map((part) => part.trim());
|
|
34
|
+
if (applies && key && value) settings[key.toLowerCase()] = value.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return settings;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The project's style: Prettier's own, then what `.editorconfig` sets, mapped as Prettier's `editorConfigToPrettier` maps it, then a `.prettierrc.json` if the project has one. */
|
|
41
|
+
static style(root: string): Options {
|
|
42
|
+
const style: Options = {};
|
|
43
|
+
const editorconfig = join(root, ".editorconfig");
|
|
44
|
+
|
|
45
|
+
if (existsSync(editorconfig)) {
|
|
46
|
+
const settings = Format.editorconfig(readFileSync(editorconfig, "utf8"));
|
|
47
|
+
const whole = (value: string | undefined) => (value && /^[1-9]\d*$/.test(value) ? Number(value) : undefined);
|
|
48
|
+
|
|
49
|
+
// What the EditorConfig library fills in before Prettier reads the settings.
|
|
50
|
+
if (settings.indent_style === "tab" && !settings.indent_size) settings.indent_size = "tab";
|
|
51
|
+
if (settings.indent_size && !settings.tab_width && settings.indent_size !== "tab") settings.tab_width = settings.indent_size;
|
|
52
|
+
if (settings.indent_size === "tab" && settings.tab_width) settings.indent_size = settings.tab_width;
|
|
53
|
+
|
|
54
|
+
if (settings.indent_style === "space") style.useTabs = false;
|
|
55
|
+
else if (settings.indent_style === "tab" || settings.indent_size === "tab") style.useTabs = true;
|
|
56
|
+
|
|
57
|
+
const width = style.useTabs === false && whole(settings.indent_size) ? whole(settings.indent_size) : whole(settings.tab_width);
|
|
58
|
+
if (width) style.tabWidth = width;
|
|
59
|
+
|
|
60
|
+
if (settings.max_line_length === "off") style.printWidth = Number.POSITIVE_INFINITY;
|
|
61
|
+
else if (whole(settings.max_line_length)) style.printWidth = whole(settings.max_line_length);
|
|
62
|
+
|
|
63
|
+
if (settings.quote_type === "single") style.singleQuote = true;
|
|
64
|
+
else if (settings.quote_type === "double") style.singleQuote = false;
|
|
65
|
+
|
|
66
|
+
if (settings.end_of_line === "lf" || settings.end_of_line === "crlf" || settings.end_of_line === "cr")
|
|
67
|
+
style.endOfLine = settings.end_of_line;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const own = join(root, ".prettierrc.json");
|
|
71
|
+
return existsSync(own) ? { ...style, ...(JSON.parse(readFileSync(own, "utf8")) as Options) } : style;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Every TypeScript file of the project, relative to it. A JSON file is formatted only when named. */
|
|
75
|
+
static files(root: string, directory = root): string[] {
|
|
76
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
77
|
+
if (entry.isDirectory()) return Format.SKIPPED.has(entry.name) ? [] : Format.files(root, join(directory, entry.name));
|
|
78
|
+
return entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") ? [relative(root, join(directory, entry.name))] : [];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A file's text in the style. */
|
|
83
|
+
static async text(text: string, file: string, style: Options): Promise<string> {
|
|
84
|
+
const parser = file.endsWith(".json") ? "json" : "typescript";
|
|
85
|
+
return format(text, { ...style, parser, plugins: [estree, typescript, babel] });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Format the files named, or every file of the project. With `check`, change nothing and exit 1 when a file is not in the style. */
|
|
89
|
+
static async run(root: string, named: readonly string[], check: boolean): Promise<void> {
|
|
90
|
+
const style = Format.style(root);
|
|
91
|
+
const files = named.length ? named.map((file) => relative(root, resolve(process.cwd(), file))) : Format.files(root);
|
|
92
|
+
const off: string[] = [];
|
|
93
|
+
|
|
94
|
+
await Ui.wait(`Reading ${files.length} file(s)`, async () => {
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
const path = join(root, file);
|
|
97
|
+
const before = readFileSync(path, "utf8");
|
|
98
|
+
let after: string;
|
|
99
|
+
try {
|
|
100
|
+
after = await Format.text(before, file, style);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
Ui.fail(`${file}: ${(error as Error).message.split("\n")[0]}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (after === before) continue;
|
|
106
|
+
off.push(file);
|
|
107
|
+
if (!check) writeFileSync(path, after);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (check) {
|
|
112
|
+
for (const file of off) Ui.warn(`${file} is not in the project's style`);
|
|
113
|
+
if (off.length) Ui.fail(`\n${off.length} file(s) to tidy: run circuit fmt.`);
|
|
114
|
+
Ui.done(`${files.length} file(s), all in style.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Ui.done(
|
|
119
|
+
off.length
|
|
120
|
+
? `Tidied ${off.length} of ${files.length} file(s): spacing and line breaks only.`
|
|
121
|
+
: `${files.length} file(s), all in style already.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The git hook a project runs before each commit, whatever makes the commit: a terminal, VS Code or GitHub Desktop. The hook is a file in `.circuit/hooks`, committed with the project, that calls `circuit hook pre-commit`. git finds it through `core.hooksPath`, a setting a clone does not carry, so Circuit sets it each time it runs in a project that lacks it.
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { SecretsFile } from "../core/secrets-file";
|
|
7
|
+
|
|
8
|
+
export class GitHooks {
|
|
9
|
+
/** Where the hooks are, in the project. */
|
|
10
|
+
static readonly FOLDER = ".circuit/hooks";
|
|
11
|
+
|
|
12
|
+
static readonly PRE_COMMIT = `#!/bin/sh
|
|
13
|
+
# Written by Circuit. Before each commit: every secret encrypted, and a project that validates.
|
|
14
|
+
if ! command -v circuit >/dev/null 2>&1; then
|
|
15
|
+
echo "Circuit is not installed here, so this commit was not checked and was stopped. Install Circuit: https://circuit.tako.id" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
exec circuit hook pre-commit
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
private static git(root: string, args: string[]): { ok: boolean; out: string } {
|
|
22
|
+
// `env` given, so a change to process.env reaches git: Bun passes its starting environment otherwise.
|
|
23
|
+
const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8", env: process.env });
|
|
24
|
+
return { ok: !result.error && result.status === 0, out: result.stdout ?? "" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static gitInstalled(): boolean {
|
|
28
|
+
return !spawnSync("git", ["--version"], { encoding: "utf8" }).error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The folder is in a git repository. */
|
|
32
|
+
static isRepository(root: string): boolean {
|
|
33
|
+
return GitHooks.git(root, ["rev-parse", "--git-dir"]).ok;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Start a repository on `main`. */
|
|
37
|
+
static init(root: string): void {
|
|
38
|
+
if (!GitHooks.git(root, ["init", "--initial-branch=main"]).ok) throw new Error("git could not start a repository here");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write the hook and point git at it, in a git repository. Gives back what changed, to tell the person, or undefined when nothing did. A repository that sends its hooks elsewhere, such as to husky, is left alone.
|
|
43
|
+
*/
|
|
44
|
+
static install(root: string): string | undefined {
|
|
45
|
+
if (!GitHooks.isRepository(root)) return undefined;
|
|
46
|
+
|
|
47
|
+
const path = join(root, GitHooks.FOLDER, "pre-commit");
|
|
48
|
+
const changes: string[] = [];
|
|
49
|
+
|
|
50
|
+
if (!existsSync(path) || readFileSync(path, "utf8") !== GitHooks.PRE_COMMIT) {
|
|
51
|
+
mkdirSync(join(root, GitHooks.FOLDER), { recursive: true });
|
|
52
|
+
writeFileSync(path, GitHooks.PRE_COMMIT);
|
|
53
|
+
changes.push(`wrote ${GitHooks.FOLDER}/pre-commit`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A clone on Linux or macOS takes the file without its execute bit when it was committed from Windows.
|
|
57
|
+
if (process.platform !== "win32") chmodSync(path, 0o755);
|
|
58
|
+
|
|
59
|
+
const current = GitHooks.git(root, ["config", "--get", "core.hooksPath"]).out.trim();
|
|
60
|
+
if (!current) {
|
|
61
|
+
GitHooks.git(root, ["config", "core.hooksPath", GitHooks.FOLDER]);
|
|
62
|
+
changes.push("git now checks each commit for secrets in plain text");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return changes.length ? changes.join(", ") : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Problems with the secrets files staged, `files` relative to the project: a value in plain text, or a password written as itself. Empty when the commit may go on. */
|
|
69
|
+
static stagedProblems(root: string, files: readonly string[]): string[] {
|
|
70
|
+
const top = GitHooks.git(root, ["rev-parse", "--show-toplevel"]).out.trim();
|
|
71
|
+
const prefix = GitHooks.git(root, ["rev-parse", "--show-prefix"]).out.trim();
|
|
72
|
+
const wanted = new Set(files.map((file) => `${prefix}${file.replace(/\\/g, "/")}`));
|
|
73
|
+
const staged = GitHooks.git(root, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]).out.split("\n").filter(Boolean);
|
|
74
|
+
|
|
75
|
+
return staged
|
|
76
|
+
.filter((file) => wanted.has(file))
|
|
77
|
+
.flatMap((file) => SecretsFile.problemsIn(GitHooks.git(top || root, ["show", `:${file}`]).out, file));
|
|
78
|
+
}
|
|
79
|
+
}
|