@jameslovespancakes/pi-plus 1.0.0 → 1.0.1

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 (40) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +190 -190
  3. package/config/pi-plus.example.json +60 -60
  4. package/config/skills/model-routing/SKILL.md +86 -86
  5. package/images/pi-plus.svg +10 -10
  6. package/package.json +67 -67
  7. package/server/board-server.mjs +641 -641
  8. package/server/package.json +17 -17
  9. package/src/core/accounts/registry.ts +93 -93
  10. package/src/core/anthropic/client-identity.ts +241 -241
  11. package/src/core/catalog/quality.ts +314 -314
  12. package/src/core/config.ts +169 -169
  13. package/src/core/env.ts +58 -58
  14. package/src/core/exec/process.ts +146 -146
  15. package/src/core/exec/ssh-config.ts +157 -157
  16. package/src/core/policy/policy.ts +183 -183
  17. package/src/core/quota/pool.ts +64 -64
  18. package/src/core/quota/usage-source.ts +289 -289
  19. package/src/core/store.ts +43 -43
  20. package/src/domains/agents/board-setup.ts +409 -409
  21. package/src/domains/agents/index.ts +462 -462
  22. package/src/domains/models/catalog-tool.ts +361 -361
  23. package/src/domains/models/index.ts +14 -14
  24. package/src/domains/models/policy-gate.ts +169 -169
  25. package/src/domains/models/provider-picker.ts +207 -207
  26. package/src/domains/remote/config-path.ts +41 -41
  27. package/src/domains/remote/index.ts +866 -866
  28. package/src/domains/remote/setup.ts +425 -425
  29. package/src/domains/setup/index.ts +220 -220
  30. package/src/domains/subscriptions/accounts.ts +242 -242
  31. package/src/domains/subscriptions/footer.ts +182 -182
  32. package/src/domains/subscriptions/index.ts +42 -42
  33. package/src/domains/subscriptions/provider.ts +219 -219
  34. package/src/domains/subscriptions/providers/anthropic.ts +149 -149
  35. package/src/domains/subscriptions/providers/codex.ts +148 -148
  36. package/src/domains/subscriptions/routing.ts +72 -72
  37. package/src/services/usage-service.ts +186 -186
  38. package/src/ui/format.ts +73 -73
  39. package/src/ui/usage-bars.ts +154 -154
  40. package/src/vendor/anthropic.ts +109 -109
@@ -1,146 +1,146 @@
1
- import { spawn } from "node:child_process";
2
- import { createReadStream } from "node:fs";
3
-
4
- /**
5
- * Process and SSH execution primitives.
6
- *
7
- * Extracted from the remote-jobs extension so anything needing a bounded child
8
- * process gets the same timeout, abort and output-cap behaviour.
9
- */
10
-
11
- export const SSH_ARGS = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=8"];
12
-
13
- const PROCESS_TAIL_CHARS = 2 * 1024 * 1024;
14
-
15
- export interface ProcessResult {
16
- code: number;
17
- stdout: string;
18
- stderr: string;
19
- timedOut: boolean;
20
- aborted: boolean;
21
- totalOutputBytes: number;
22
- }
23
-
24
- export interface RunOptions {
25
- cwd?: string;
26
- input?: string | Buffer | { file: string };
27
- timeoutSeconds?: number;
28
- signal?: AbortSignal;
29
- onData?: (chunk: string) => void;
30
- }
31
-
32
- /** Keeps only the trailing window of a stream so long builds cannot exhaust memory. */
33
- export function appendTail(current: string, chunk: string): string {
34
- const next = current + chunk;
35
- return next.length > PROCESS_TAIL_CHARS ? next.slice(-PROCESS_TAIL_CHARS) : next;
36
- }
37
-
38
- export function runProcess(command: string, args: string[], options: RunOptions = {}): Promise<ProcessResult> {
39
- return new Promise((resolvePromise, reject) => {
40
- const child = spawn(command, args, {
41
- cwd: options.cwd,
42
- shell: false,
43
- stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
44
- windowsHide: true,
45
- });
46
-
47
- let stdout = "";
48
- let stderr = "";
49
- let totalOutputBytes = 0;
50
- let timedOut = false;
51
- let aborted = false;
52
- let settled = false;
53
- let timer: NodeJS.Timeout | undefined;
54
-
55
- const finishReject = (error: Error) => {
56
- if (settled) return;
57
- settled = true;
58
- if (timer) clearTimeout(timer);
59
- reject(error);
60
- };
61
-
62
- const kill = () => {
63
- try {
64
- child.kill("SIGTERM");
65
- } catch {
66
- // Process already exited.
67
- }
68
- };
69
-
70
- child.stdout?.on("data", (data: Buffer) => {
71
- const text = data.toString("utf8");
72
- totalOutputBytes += data.length;
73
- stdout = appendTail(stdout, text);
74
- options.onData?.(text);
75
- });
76
-
77
- child.stderr?.on("data", (data: Buffer) => {
78
- const text = data.toString("utf8");
79
- totalOutputBytes += data.length;
80
- stderr = appendTail(stderr, text);
81
- options.onData?.(text);
82
- });
83
-
84
- child.on("error", finishReject);
85
-
86
- if (options.timeoutSeconds) {
87
- timer = setTimeout(() => {
88
- timedOut = true;
89
- kill();
90
- }, options.timeoutSeconds * 1000);
91
- }
92
-
93
- const onAbort = () => {
94
- aborted = true;
95
- kill();
96
- };
97
-
98
- if (options.signal) {
99
- if (options.signal.aborted) onAbort();
100
- else options.signal.addEventListener("abort", onAbort, { once: true });
101
- }
102
-
103
- if (typeof options.input === "string" || Buffer.isBuffer(options.input)) {
104
- child.stdin?.end(options.input);
105
- } else if (options.input?.file && child.stdin) {
106
- const target = child.stdin;
107
- const stream = createReadStream(options.input.file);
108
- stream.on("error", (error) => {
109
- kill();
110
- finishReject(error);
111
- });
112
- stream.pipe(target);
113
- }
114
-
115
- child.on("close", (code) => {
116
- if (settled) return;
117
- settled = true;
118
- if (timer) clearTimeout(timer);
119
- options.signal?.removeEventListener("abort", onAbort);
120
- resolvePromise({ code: code ?? 1, stdout, stderr, timedOut, aborted, totalOutputBytes });
121
- });
122
- });
123
- }
124
-
125
- export async function runLocal(command: string, args: string[], cwd?: string): Promise<ProcessResult> {
126
- return runProcess(command, args, { cwd, timeoutSeconds: 60 });
127
- }
128
-
129
- export async function runSshCommand(
130
- host: string,
131
- remoteCommand: string,
132
- options: RunOptions = {},
133
- extraArgs: string[] = [],
134
- ): Promise<ProcessResult> {
135
- return runProcess("ssh", [...SSH_ARGS, ...extraArgs, host, remoteCommand], options);
136
- }
137
-
138
- /** POSIX single-quote escaping for values interpolated into remote scripts. */
139
- export function shQuote(value: string): string {
140
- return `'${value.replace(/'/g, `'"'"'`)}'`;
141
- }
142
-
143
- export function rootAssignment(root: string): string {
144
- if (root.startsWith("~/")) return `ROOT="$HOME"/${shQuote(root.slice(2))}`;
145
- return `ROOT=${shQuote(root)}`;
146
- }
1
+ import { spawn } from "node:child_process";
2
+ import { createReadStream } from "node:fs";
3
+
4
+ /**
5
+ * Process and SSH execution primitives.
6
+ *
7
+ * Extracted from the remote-jobs extension so anything needing a bounded child
8
+ * process gets the same timeout, abort and output-cap behaviour.
9
+ */
10
+
11
+ export const SSH_ARGS = ["-o", "BatchMode=yes", "-o", "ConnectTimeout=8"];
12
+
13
+ const PROCESS_TAIL_CHARS = 2 * 1024 * 1024;
14
+
15
+ export interface ProcessResult {
16
+ code: number;
17
+ stdout: string;
18
+ stderr: string;
19
+ timedOut: boolean;
20
+ aborted: boolean;
21
+ totalOutputBytes: number;
22
+ }
23
+
24
+ export interface RunOptions {
25
+ cwd?: string;
26
+ input?: string | Buffer | { file: string };
27
+ timeoutSeconds?: number;
28
+ signal?: AbortSignal;
29
+ onData?: (chunk: string) => void;
30
+ }
31
+
32
+ /** Keeps only the trailing window of a stream so long builds cannot exhaust memory. */
33
+ export function appendTail(current: string, chunk: string): string {
34
+ const next = current + chunk;
35
+ return next.length > PROCESS_TAIL_CHARS ? next.slice(-PROCESS_TAIL_CHARS) : next;
36
+ }
37
+
38
+ export function runProcess(command: string, args: string[], options: RunOptions = {}): Promise<ProcessResult> {
39
+ return new Promise((resolvePromise, reject) => {
40
+ const child = spawn(command, args, {
41
+ cwd: options.cwd,
42
+ shell: false,
43
+ stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
44
+ windowsHide: true,
45
+ });
46
+
47
+ let stdout = "";
48
+ let stderr = "";
49
+ let totalOutputBytes = 0;
50
+ let timedOut = false;
51
+ let aborted = false;
52
+ let settled = false;
53
+ let timer: NodeJS.Timeout | undefined;
54
+
55
+ const finishReject = (error: Error) => {
56
+ if (settled) return;
57
+ settled = true;
58
+ if (timer) clearTimeout(timer);
59
+ reject(error);
60
+ };
61
+
62
+ const kill = () => {
63
+ try {
64
+ child.kill("SIGTERM");
65
+ } catch {
66
+ // Process already exited.
67
+ }
68
+ };
69
+
70
+ child.stdout?.on("data", (data: Buffer) => {
71
+ const text = data.toString("utf8");
72
+ totalOutputBytes += data.length;
73
+ stdout = appendTail(stdout, text);
74
+ options.onData?.(text);
75
+ });
76
+
77
+ child.stderr?.on("data", (data: Buffer) => {
78
+ const text = data.toString("utf8");
79
+ totalOutputBytes += data.length;
80
+ stderr = appendTail(stderr, text);
81
+ options.onData?.(text);
82
+ });
83
+
84
+ child.on("error", finishReject);
85
+
86
+ if (options.timeoutSeconds) {
87
+ timer = setTimeout(() => {
88
+ timedOut = true;
89
+ kill();
90
+ }, options.timeoutSeconds * 1000);
91
+ }
92
+
93
+ const onAbort = () => {
94
+ aborted = true;
95
+ kill();
96
+ };
97
+
98
+ if (options.signal) {
99
+ if (options.signal.aborted) onAbort();
100
+ else options.signal.addEventListener("abort", onAbort, { once: true });
101
+ }
102
+
103
+ if (typeof options.input === "string" || Buffer.isBuffer(options.input)) {
104
+ child.stdin?.end(options.input);
105
+ } else if (options.input?.file && child.stdin) {
106
+ const target = child.stdin;
107
+ const stream = createReadStream(options.input.file);
108
+ stream.on("error", (error) => {
109
+ kill();
110
+ finishReject(error);
111
+ });
112
+ stream.pipe(target);
113
+ }
114
+
115
+ child.on("close", (code) => {
116
+ if (settled) return;
117
+ settled = true;
118
+ if (timer) clearTimeout(timer);
119
+ options.signal?.removeEventListener("abort", onAbort);
120
+ resolvePromise({ code: code ?? 1, stdout, stderr, timedOut, aborted, totalOutputBytes });
121
+ });
122
+ });
123
+ }
124
+
125
+ export async function runLocal(command: string, args: string[], cwd?: string): Promise<ProcessResult> {
126
+ return runProcess(command, args, { cwd, timeoutSeconds: 60 });
127
+ }
128
+
129
+ export async function runSshCommand(
130
+ host: string,
131
+ remoteCommand: string,
132
+ options: RunOptions = {},
133
+ extraArgs: string[] = [],
134
+ ): Promise<ProcessResult> {
135
+ return runProcess("ssh", [...SSH_ARGS, ...extraArgs, host, remoteCommand], options);
136
+ }
137
+
138
+ /** POSIX single-quote escaping for values interpolated into remote scripts. */
139
+ export function shQuote(value: string): string {
140
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
141
+ }
142
+
143
+ export function rootAssignment(root: string): string {
144
+ if (root.startsWith("~/")) return `ROOT="$HOME"/${shQuote(root.slice(2))}`;
145
+ return `ROOT=${shQuote(root)}`;
146
+ }
@@ -1,157 +1,157 @@
1
- import { readFileSync } from "node:fs";
2
- import { globSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { dirname, isAbsolute, join } from "node:path";
5
-
6
- /**
7
- * Minimal `~/.ssh/config` reader.
8
- *
9
- * Deliberately narrow: it extracts only what is needed to offer a host as a
10
- * remote worker: the alias, HostName, User, Port, and whether an IdentityFile
11
- * is configured. It never reads key material, and callers must keep the results
12
- * in the UI layer: host aliases frequently name internal infrastructure and have
13
- * no business being sent to a model provider.
14
- */
15
-
16
- export interface SshHost {
17
- /** The `Host` alias, which is what you pass to `ssh`. */
18
- alias: string;
19
- hostName?: string;
20
- user?: string;
21
- port?: number;
22
- /** Path as written in the config. The file is never opened. */
23
- identityFile?: string;
24
- /** Config file this block came from, for display. */
25
- source: string;
26
- }
27
-
28
- function defaultConfigPath(): string {
29
- return join(homedir(), ".ssh", "config");
30
- }
31
-
32
- /** `Host` patterns that cannot be connected to directly. */
33
- function isPattern(alias: string): boolean {
34
- return alias.includes("*") || alias.includes("?") || alias.startsWith("!");
35
- }
36
-
37
- function expandPath(value: string, base: string): string {
38
- if (value.startsWith("~/")) return join(homedir(), value.slice(2));
39
- return isAbsolute(value) ? value : join(base, value);
40
- }
41
-
42
- /**
43
- * Parses one config file, following `Include` directives.
44
- * `seen` guards against include cycles.
45
- */
46
- function parseFile(path: string, seen: Set<string>, out: SshHost[]): void {
47
- if (seen.has(path)) return;
48
- seen.add(path);
49
-
50
- let text: string;
51
- try {
52
- text = readFileSync(path, "utf8");
53
- } catch {
54
- return; // Missing or unreadable config is simply "no hosts".
55
- }
56
-
57
- let current: SshHost | undefined;
58
- const push = () => {
59
- if (current && !isPattern(current.alias)) out.push(current);
60
- current = undefined;
61
- };
62
-
63
- for (const rawLine of text.split(/\r?\n/)) {
64
- const line = rawLine.trim();
65
- if (!line || line.startsWith("#")) continue;
66
-
67
- // Keywords are case-insensitive; separator is whitespace or '='.
68
- const match = /^([A-Za-z]+)[\s=]+(.*)$/.exec(line);
69
- if (!match) continue;
70
- const keyword = match[1].toLowerCase();
71
- const value = match[2].trim().replace(/^"(.*)"$/, "$1");
72
- if (!value) continue;
73
-
74
- if (keyword === "host") {
75
- push();
76
- // `Host a b c` declares several aliases; the first connectable one wins.
77
- const alias = value.split(/\s+/).find((candidate) => !isPattern(candidate));
78
- if (alias) current = { alias, source: path };
79
- continue;
80
- }
81
-
82
- if (keyword === "include") {
83
- // Includes are resolved relative to the containing file's directory.
84
- for (const pattern of value.split(/\s+/)) {
85
- const resolved = expandPath(pattern, dirname(path));
86
- let matches: string[] = [];
87
- try {
88
- matches = globSync(resolved);
89
- } catch {
90
- matches = [];
91
- }
92
- // A literal path that does not glob should still be attempted.
93
- for (const file of matches.length > 0 ? matches : [resolved]) parseFile(file, seen, out);
94
- }
95
- continue;
96
- }
97
-
98
- if (!current) continue;
99
- switch (keyword) {
100
- case "hostname":
101
- current.hostName = value;
102
- break;
103
- case "user":
104
- current.user = value;
105
- break;
106
- case "port": {
107
- const port = Number(value);
108
- if (Number.isInteger(port) && port > 0 && port < 65536) current.port = port;
109
- break;
110
- }
111
- case "identityfile":
112
- // Recorded as a path only. The key itself is never read.
113
- current.identityFile ??= value;
114
- break;
115
- default:
116
- break;
117
- }
118
- }
119
- push();
120
- }
121
-
122
- /** Every connectable host in the user's SSH config, in file order. */
123
- export function readSshHosts(path = defaultConfigPath()): SshHost[] {
124
- const hosts: SshHost[] = [];
125
- parseFile(path, new Set(), hosts);
126
-
127
- // A later duplicate alias never overrides the first, matching ssh semantics.
128
- const byAlias = new Map<string, SshHost>();
129
- for (const host of hosts) if (!byAlias.has(host.alias)) byAlias.set(host.alias, host);
130
- return [...byAlias.values()];
131
- }
132
-
133
- /** `deploy@203.0.113.9:2222` -> parts. Bare `host` is valid. */
134
- export function parseTarget(input: string): { user?: string; host: string; port?: number } | undefined {
135
- const trimmed = input.trim();
136
- if (!trimmed || /\s/.test(trimmed)) return undefined;
137
-
138
- const at = trimmed.lastIndexOf("@");
139
- if (at === 0) return undefined; // "@host" has an empty user
140
- const user = at > 0 ? trimmed.slice(0, at) : undefined;
141
- let rest = at > 0 ? trimmed.slice(at + 1) : trimmed;
142
-
143
- let port: number | undefined;
144
- // Only treat a trailing :N as a port; IPv6 literals keep their colons.
145
- const portMatch = /^(.*):(\d+)$/.exec(rest);
146
- if (portMatch && !portMatch[1].includes(":")) {
147
- rest = portMatch[1];
148
- port = Number(portMatch[2]);
149
- if (!Number.isInteger(port) || port <= 0 || port >= 65536) return undefined;
150
- }
151
-
152
- if (!rest) return undefined;
153
- // A leftover single colon is a malformed port ("host:abc"). Genuine IPv6
154
- // literals always carry at least two.
155
- if (rest.includes(":") && (rest.match(/:/g)?.length ?? 0) < 2) return undefined;
156
- return { user, host: rest, port };
157
- }
1
+ import { readFileSync } from "node:fs";
2
+ import { globSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
+
6
+ /**
7
+ * Minimal `~/.ssh/config` reader.
8
+ *
9
+ * Deliberately narrow: it extracts only what is needed to offer a host as a
10
+ * remote worker: the alias, HostName, User, Port, and whether an IdentityFile
11
+ * is configured. It never reads key material, and callers must keep the results
12
+ * in the UI layer: host aliases frequently name internal infrastructure and have
13
+ * no business being sent to a model provider.
14
+ */
15
+
16
+ export interface SshHost {
17
+ /** The `Host` alias, which is what you pass to `ssh`. */
18
+ alias: string;
19
+ hostName?: string;
20
+ user?: string;
21
+ port?: number;
22
+ /** Path as written in the config. The file is never opened. */
23
+ identityFile?: string;
24
+ /** Config file this block came from, for display. */
25
+ source: string;
26
+ }
27
+
28
+ function defaultConfigPath(): string {
29
+ return join(homedir(), ".ssh", "config");
30
+ }
31
+
32
+ /** `Host` patterns that cannot be connected to directly. */
33
+ function isPattern(alias: string): boolean {
34
+ return alias.includes("*") || alias.includes("?") || alias.startsWith("!");
35
+ }
36
+
37
+ function expandPath(value: string, base: string): string {
38
+ if (value.startsWith("~/")) return join(homedir(), value.slice(2));
39
+ return isAbsolute(value) ? value : join(base, value);
40
+ }
41
+
42
+ /**
43
+ * Parses one config file, following `Include` directives.
44
+ * `seen` guards against include cycles.
45
+ */
46
+ function parseFile(path: string, seen: Set<string>, out: SshHost[]): void {
47
+ if (seen.has(path)) return;
48
+ seen.add(path);
49
+
50
+ let text: string;
51
+ try {
52
+ text = readFileSync(path, "utf8");
53
+ } catch {
54
+ return; // Missing or unreadable config is simply "no hosts".
55
+ }
56
+
57
+ let current: SshHost | undefined;
58
+ const push = () => {
59
+ if (current && !isPattern(current.alias)) out.push(current);
60
+ current = undefined;
61
+ };
62
+
63
+ for (const rawLine of text.split(/\r?\n/)) {
64
+ const line = rawLine.trim();
65
+ if (!line || line.startsWith("#")) continue;
66
+
67
+ // Keywords are case-insensitive; separator is whitespace or '='.
68
+ const match = /^([A-Za-z]+)[\s=]+(.*)$/.exec(line);
69
+ if (!match) continue;
70
+ const keyword = match[1].toLowerCase();
71
+ const value = match[2].trim().replace(/^"(.*)"$/, "$1");
72
+ if (!value) continue;
73
+
74
+ if (keyword === "host") {
75
+ push();
76
+ // `Host a b c` declares several aliases; the first connectable one wins.
77
+ const alias = value.split(/\s+/).find((candidate) => !isPattern(candidate));
78
+ if (alias) current = { alias, source: path };
79
+ continue;
80
+ }
81
+
82
+ if (keyword === "include") {
83
+ // Includes are resolved relative to the containing file's directory.
84
+ for (const pattern of value.split(/\s+/)) {
85
+ const resolved = expandPath(pattern, dirname(path));
86
+ let matches: string[] = [];
87
+ try {
88
+ matches = globSync(resolved);
89
+ } catch {
90
+ matches = [];
91
+ }
92
+ // A literal path that does not glob should still be attempted.
93
+ for (const file of matches.length > 0 ? matches : [resolved]) parseFile(file, seen, out);
94
+ }
95
+ continue;
96
+ }
97
+
98
+ if (!current) continue;
99
+ switch (keyword) {
100
+ case "hostname":
101
+ current.hostName = value;
102
+ break;
103
+ case "user":
104
+ current.user = value;
105
+ break;
106
+ case "port": {
107
+ const port = Number(value);
108
+ if (Number.isInteger(port) && port > 0 && port < 65536) current.port = port;
109
+ break;
110
+ }
111
+ case "identityfile":
112
+ // Recorded as a path only. The key itself is never read.
113
+ current.identityFile ??= value;
114
+ break;
115
+ default:
116
+ break;
117
+ }
118
+ }
119
+ push();
120
+ }
121
+
122
+ /** Every connectable host in the user's SSH config, in file order. */
123
+ export function readSshHosts(path = defaultConfigPath()): SshHost[] {
124
+ const hosts: SshHost[] = [];
125
+ parseFile(path, new Set(), hosts);
126
+
127
+ // A later duplicate alias never overrides the first, matching ssh semantics.
128
+ const byAlias = new Map<string, SshHost>();
129
+ for (const host of hosts) if (!byAlias.has(host.alias)) byAlias.set(host.alias, host);
130
+ return [...byAlias.values()];
131
+ }
132
+
133
+ /** `deploy@203.0.113.9:2222` -> parts. Bare `host` is valid. */
134
+ export function parseTarget(input: string): { user?: string; host: string; port?: number } | undefined {
135
+ const trimmed = input.trim();
136
+ if (!trimmed || /\s/.test(trimmed)) return undefined;
137
+
138
+ const at = trimmed.lastIndexOf("@");
139
+ if (at === 0) return undefined; // "@host" has an empty user
140
+ const user = at > 0 ? trimmed.slice(0, at) : undefined;
141
+ let rest = at > 0 ? trimmed.slice(at + 1) : trimmed;
142
+
143
+ let port: number | undefined;
144
+ // Only treat a trailing :N as a port; IPv6 literals keep their colons.
145
+ const portMatch = /^(.*):(\d+)$/.exec(rest);
146
+ if (portMatch && !portMatch[1].includes(":")) {
147
+ rest = portMatch[1];
148
+ port = Number(portMatch[2]);
149
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536) return undefined;
150
+ }
151
+
152
+ if (!rest) return undefined;
153
+ // A leftover single colon is a malformed port ("host:abc"). Genuine IPv6
154
+ // literals always carry at least two.
155
+ if (rest.includes(":") && (rest.match(/:/g)?.length ?? 0) < 2) return undefined;
156
+ return { user, host: rest, port };
157
+ }