@dbx-tools/shared 0.1.18

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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * HTTP header helpers shared across AppKit plugins: framework
3
+ * agnostic readers for HTTP headers and cookies that work uniformly
4
+ * across Express, Node `IncomingMessage`, WHATWG `Request` / `Response`
5
+ * / `Headers`, Hono, and any object that exposes a `headers` field of
6
+ * one of those shapes.
7
+ *
8
+ * Public API: {@link forEachHeaderValue}, {@link parseCookies}.
9
+ * Everything else (the header guards `isHeaders` / `isWrapped` /
10
+ * `unwrap`, the single cookie-header parser `parseCookieString`) is
11
+ * private to this module.
12
+ *
13
+ * URL parsing and path joining moved to `netUtils` (`./net.browser.ts`)
14
+ * so the URL surface lives next to the rest of the browser-safe
15
+ * networking helpers. The Databricks-aware REST helper that used to
16
+ * live here moved to `apiUtils.fetchApi` (`./api.ts`) so this module
17
+ * can stay dependency-free and browser-safe.
18
+ */
19
+ // ────────────────────────────────────────────────────────────────
20
+ // Header helpers
21
+ // ────────────────────────────────────────────────────────────────
22
+ /**
23
+ * Invokes `consumer` once per value for `headerName`, case-insensitive.
24
+ *
25
+ * - **Record input:** if the field is an array (e.g. repeated `Set-Cookie`),
26
+ * `consumer` runs once per array item.
27
+ * - **`Headers` input:** uses `get(name)` (which spec-joins repeats with
28
+ * `, `) except for `Set-Cookie`, which uses `getSetCookie()` so each
29
+ * cookie is delivered separately.
30
+ *
31
+ * @example
32
+ * forEachHeaderValue(req, "x-trace-id", (v) => spans.push(v)); // Express
33
+ * forEachHeaderValue(c.req.raw, "set-cookie", (v) => log(v)); // Hono
34
+ * forEachHeaderValue(headersInstance, "cookie", parse); // fetch
35
+ */
36
+ export function forEachHeaderValue(input, headerName, consumer) {
37
+ const headers = unwrap(input);
38
+ if (!headers)
39
+ return;
40
+ const target = headerName.toLowerCase();
41
+ if (isHeaders(headers)) {
42
+ // `Headers.get` joins repeated values with `, ` per spec, which
43
+ // mangles `Set-Cookie` (cookies legitimately contain commas in
44
+ // their `expires=` attribute). `getSetCookie` is the dedicated
45
+ // splitter and is the only safe path for that header.
46
+ if (target === "set-cookie") {
47
+ for (const value of headers.getSetCookie())
48
+ consumer(value);
49
+ return;
50
+ }
51
+ const value = headers.get(headerName);
52
+ if (value !== null)
53
+ consumer(value);
54
+ return;
55
+ }
56
+ for (const [key, value] of Object.entries(headers)) {
57
+ if (value == null || key.toLowerCase() !== target)
58
+ continue;
59
+ if (Array.isArray(value)) {
60
+ for (const item of value)
61
+ consumer(item);
62
+ }
63
+ else {
64
+ consumer(value);
65
+ }
66
+ }
67
+ }
68
+ /**
69
+ * Parses `Cookie` header values into a name-to-value map (URI-decoded).
70
+ *
71
+ * Accepts:
72
+ *
73
+ * - A raw `Cookie` string (`"a=1; b=2"`).
74
+ * - An array of such strings (e.g. multiple `Cookie` headers).
75
+ * - Any {@link HeaderLike}: a WHATWG `Headers` instance, a header
76
+ * record, or a request-like object with a `headers` field.
77
+ *
78
+ * First occurrence of each cookie name wins; later duplicates are ignored.
79
+ *
80
+ * @example
81
+ * parseCookies("session=abc; theme=dark");
82
+ * // { session: "abc", theme: "dark" }
83
+ *
84
+ * parseCookies(req); // Express / Node
85
+ * parseCookies(c.req.raw); // Hono
86
+ * parseCookies(request); // fetch Request
87
+ * parseCookies(request.headers); // WHATWG Headers directly
88
+ */
89
+ export function parseCookies(input) {
90
+ if (input == null)
91
+ return {};
92
+ const out = {};
93
+ if (typeof input === "string") {
94
+ parseCookieString(input, out);
95
+ return out;
96
+ }
97
+ if (Array.isArray(input)) {
98
+ for (const item of input) {
99
+ if (typeof item === "string")
100
+ parseCookieString(item, out);
101
+ }
102
+ return out;
103
+ }
104
+ forEachHeaderValue(input, "cookie", (value) => {
105
+ parseCookieString(value, out);
106
+ });
107
+ return out;
108
+ }
109
+ // ────────────────────────────────────────────────────────────────
110
+ // Private helpers
111
+ // ────────────────────────────────────────────────────────────────
112
+ /**
113
+ * Type guard for WHATWG `Headers`. Duck-types on the two methods that
114
+ * matter to this module (`get` and `getSetCookie`) so polyfilled
115
+ * implementations and Hono's `HonoHeaders` are accepted without
116
+ * pulling `Headers` in as a hard dependency.
117
+ */
118
+ function isHeaders(value) {
119
+ return (typeof value === "object" &&
120
+ value !== null &&
121
+ typeof value.get === "function" &&
122
+ typeof value.getSetCookie === "function");
123
+ }
124
+ /**
125
+ * `HeaderRecord` is an index signature, so `"headers" in input` cannot
126
+ * discriminate it from the wrapped `{ headers }` shape at the type
127
+ * level. This guard inspects the runtime value of `headers`: only
128
+ * objects (`Headers` or a nested record) qualify as the wrapper shape,
129
+ * never stray string/array values that happen to live under a `headers`
130
+ * key on a header record.
131
+ */
132
+ function isWrapped(input) {
133
+ const headers = input.headers;
134
+ return headers != null && typeof headers === "object" && !Array.isArray(headers);
135
+ }
136
+ /**
137
+ * Parse a single `Cookie`-style header string (`"a=1; b=2"`) into
138
+ * `out`. Names without a value are skipped; first occurrence wins so
139
+ * later duplicates are ignored. Cookie values are URI-decoded.
140
+ */
141
+ function parseCookieString(input, out) {
142
+ for (const part of input.split(";")) {
143
+ const eq = part.indexOf("=");
144
+ if (eq < 0)
145
+ continue;
146
+ const name = part.slice(0, eq).trim();
147
+ if (!name || name in out)
148
+ continue;
149
+ const raw = part.slice(eq + 1).trim();
150
+ out[name] = decodeURIComponent(raw);
151
+ }
152
+ }
153
+ /**
154
+ * Normalize a {@link HeaderLike} input down to either a `Headers`
155
+ * instance or a header `Record`. Returns `null` for missing input so
156
+ * callers can short-circuit without a separate nullish check.
157
+ */
158
+ function unwrap(input) {
159
+ if (input == null)
160
+ return null;
161
+ if (isHeaders(input))
162
+ return input;
163
+ if (isWrapped(input))
164
+ return input.headers;
165
+ return input;
166
+ }
@@ -0,0 +1,47 @@
1
+ import type { NameLike } from "./common.js";
2
+ /** Plugin-facing logger surface returned by {@link logger}. */
3
+ export interface Logger {
4
+ debug(message: string, attributes?: Record<string, unknown>): void;
5
+ info(message: string, attributes?: Record<string, unknown>): void;
6
+ warn(message: string, attributes?: Record<string, unknown>): void;
7
+ error(message: string, attributes?: Record<string, unknown>): void;
8
+ }
9
+ /**
10
+ * Severity ordering. A log call below the active threshold is
11
+ * discarded entirely (no string formatting, no console call). The
12
+ * threshold is read on every call from `process.env.LOG_LEVEL`,
13
+ * case-insensitive, defaulting to `info` when unset / empty /
14
+ * unrecognised. Set `LOG_LEVEL=debug` for verbose dev output,
15
+ * `LOG_LEVEL=warn` to silence info chatter in production, etc.
16
+ *
17
+ * Lazy on purpose: looking up `process.env.LOG_LEVEL` per call
18
+ * costs nothing meaningful, and it lets test runners (and other
19
+ * embedders) flip the level after `log.ts` has already been
20
+ * imported, without restarting the process or reaching into
21
+ * private state.
22
+ */
23
+ export type LogLevel = "debug" | "info" | "warn" | "error";
24
+ /**
25
+ * Build a per-plugin logger that writes to `console` with an optional
26
+ * `[plugin-name]` prefix derived from `plugin.name` or the string you pass in.
27
+ *
28
+ * Calls below `process.env.LOG_LEVEL` are discarded before any
29
+ * string work happens, so leaving `log.debug({...heavy details})`
30
+ * in production code is free as long as `LOG_LEVEL` is `info` or
31
+ * higher. Default level is `info`.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { logUtils } from "@dbx-tools/shared";
36
+ *
37
+ * class MyPlugin extends Plugin {
38
+ * private log = logUtils.logger(this);
39
+ *
40
+ * override async setup() {
41
+ * this.log.info("starting");
42
+ * this.log.warn("missing optional config", { reason: "no env var" });
43
+ * }
44
+ * }
45
+ * ```
46
+ */
47
+ export declare function logger(loggerName: NameLike | string | undefined): Logger;
@@ -0,0 +1,80 @@
1
+ const LEVEL_RANK = {
2
+ debug: 10,
3
+ info: 20,
4
+ warn: 30,
5
+ error: 40,
6
+ };
7
+ const DEFAULT_LEVEL = "info";
8
+ /**
9
+ * Read the active threshold from `process.env.LOG_LEVEL`. Recognises
10
+ * any case (`DEBUG`, `Debug`, `debug`), trims whitespace, falls back
11
+ * to {@link DEFAULT_LEVEL} when unset / empty / unrecognised.
12
+ *
13
+ * Browser-safe: `process` is undefined in browser bundles unless a
14
+ * polyfill or build-time replace is set up, so we guard the access.
15
+ * In a Vite app, set `LOG_LEVEL` via `define` config or just leave
16
+ * it - the default `info` is sane for production browser code.
17
+ */
18
+ function activeLevel() {
19
+ const env = typeof process !== "undefined" ? process.env : undefined;
20
+ const raw = env?.LOG_LEVEL?.toLowerCase().trim();
21
+ if (raw && Object.prototype.hasOwnProperty.call(LEVEL_RANK, raw)) {
22
+ return raw;
23
+ }
24
+ return DEFAULT_LEVEL;
25
+ }
26
+ /** True when calls at `level` should reach the console. */
27
+ function shouldEmit(level) {
28
+ return LEVEL_RANK[level] >= LEVEL_RANK[activeLevel()];
29
+ }
30
+ const LOGGER_NAME_REGEX = /^(?:[a-z][a-z0-9+.-]*:\/\/)?.*\/([^/.]+)(?:\.[^/]+)?$/i;
31
+ function extractLoggerName(loggerName) {
32
+ if (!loggerName)
33
+ return undefined;
34
+ else if (typeof loggerName === "string") {
35
+ const match = loggerName.match(LOGGER_NAME_REGEX);
36
+ return match?.[1] ?? loggerName;
37
+ }
38
+ else {
39
+ return extractLoggerName(loggerName?.name);
40
+ }
41
+ }
42
+ /**
43
+ * Build a per-plugin logger that writes to `console` with an optional
44
+ * `[plugin-name]` prefix derived from `plugin.name` or the string you pass in.
45
+ *
46
+ * Calls below `process.env.LOG_LEVEL` are discarded before any
47
+ * string work happens, so leaving `log.debug({...heavy details})`
48
+ * in production code is free as long as `LOG_LEVEL` is `info` or
49
+ * higher. Default level is `info`.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { logUtils } from "@dbx-tools/shared";
54
+ *
55
+ * class MyPlugin extends Plugin {
56
+ * private log = logUtils.logger(this);
57
+ *
58
+ * override async setup() {
59
+ * this.log.info("starting");
60
+ * this.log.warn("missing optional config", { reason: "no env var" });
61
+ * }
62
+ * }
63
+ * ```
64
+ */
65
+ export function logger(loggerName) {
66
+ const name = extractLoggerName(loggerName);
67
+ function log(level, consoleFn, message, attributes) {
68
+ if (!shouldEmit(level))
69
+ return;
70
+ const logMessage = name ? `[${name}] ${message}` : message;
71
+ const logArgs = [logMessage, attributes].filter(Boolean);
72
+ consoleFn(...logArgs);
73
+ }
74
+ return {
75
+ debug: (msg, attrs) => log("debug", console.debug, msg, attrs),
76
+ info: (msg, attrs) => log("info", console.info, msg, attrs),
77
+ warn: (msg, attrs) => log("warn", console.warn, msg, attrs),
78
+ error: (msg, attrs) => log("error", console.error, msg, attrs),
79
+ };
80
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Browser-safe networking helpers: URL parsing and path joining that
3
+ * gracefully handle partial inputs. No node-only imports, so this
4
+ * module is the canonical home for anything URL-shaped that also has
5
+ * to run in a Vite / Webpack / esbuild client bundle.
6
+ *
7
+ * Public API: {@link joinUrl}, {@link parseUrl}. The
8
+ * single-leading/trailing-slash stripper `stripBoundarySlashes` is
9
+ * private to this module.
10
+ *
11
+ * The server-side `./net.ts` re-exports everything here verbatim and
12
+ * tacks on its own node-only helpers (e.g. `getRandomPort`), so the
13
+ * `netUtils` namespace looks identical from both entry points.
14
+ */
15
+ /**
16
+ * One input to {@link joinUrl}: a string, a (recursively nested)
17
+ * array of segments, or `null` / `undefined` (skipped).
18
+ *
19
+ * The recursive shape lets callers compose path fragments without
20
+ * pre-flattening: `joinUrl("a", ["b", ["c", "d"]])` is valid.
21
+ *
22
+ * The array variant uses an interface (rather than a self-referential
23
+ * type alias) because Bun's TS parser bails on `type X = ... | X[] | ...`
24
+ * but accepts the equivalent `interface XArr extends Array<X>`.
25
+ */
26
+ export type UrlSegmentLike = string | UrlSegmentArray | null | undefined;
27
+ export interface UrlSegmentArray extends ReadonlyArray<UrlSegmentLike> {
28
+ }
29
+ /**
30
+ * Anything {@link parseUrl} knows how to coerce into a `URL`:
31
+ *
32
+ * - A WHATWG `URL` instance: returned as-is when no extra path is
33
+ * supplied; otherwise re-parsed with the path appended.
34
+ * - A string: parsed by the `URL` constructor; `https://` is
35
+ * auto-prefixed when no scheme is present, so bare hostnames like
36
+ * `"example.com"` round-trip into a usable URL.
37
+ * - Any object with a `url` field of the above shapes (e.g. a fetch
38
+ * `Request`, a Databricks `WorkspaceClient` config, etc.).
39
+ */
40
+ export type UrlLike = URL | string | {
41
+ url: string;
42
+ };
43
+ /**
44
+ * Join URL path segments with `/`, with three pragmatic conveniences
45
+ * for building URLs piecemeal:
46
+ *
47
+ * 1. Nullish or blank segments are dropped, so callers don't have
48
+ * to guard around optional path components.
49
+ * 2. Each string segment has any leading or trailing `/` stripped
50
+ * before joining, so `"/api"` + `"v2/"` round-trips to `/api/v2`
51
+ * regardless of how the caller terminated each part.
52
+ * 3. Array segments recurse, with their joined form inlined into
53
+ * the outer result.
54
+ *
55
+ * The result is prefixed with `/` to make it path-absolute, except
56
+ * when any segment carries an explicit `://` scheme (e.g.
57
+ * `"https://x.com"`), in which case the scheme is preserved verbatim.
58
+ *
59
+ * Returns `""` when every input was nullish or blank.
60
+ *
61
+ * @example
62
+ * joinUrl("a", "b"); // "/a/b"
63
+ * joinUrl("/api/", "/v2/", "x"); // "/api/v2/x"
64
+ * joinUrl(["a", "b"], "c"); // "/a/b/c"
65
+ * joinUrl("https://ex.com", "/api/x"); // "https://ex.com/api/x"
66
+ * joinUrl(null, "", "x", undefined); // "/x"
67
+ * joinUrl(); // ""
68
+ * joinUrl(null); // ""
69
+ */
70
+ export declare function joinUrl(...urlSegments: UrlSegmentLike[]): string;
71
+ /**
72
+ * Coerce a {@link UrlLike} input into a parsed `URL`, returning `null`
73
+ * for anything that cannot be coerced (null/undefined, empty string,
74
+ * malformed URL, an object whose `url` field doesn't parse).
75
+ *
76
+ * Mirrors WHATWG `URL.parse(...)` semantics: parse on success, `null`
77
+ * on failure - never throws.
78
+ *
79
+ * Bare hostnames are upgraded to `https://` before parsing, so callers
80
+ * can hand in user-provided values like `"workspace.cloud.databricks.com"`
81
+ * without an explicit scheme.
82
+ *
83
+ * Optional trailing `path` arguments are appended via {@link joinUrl}.
84
+ * When `input` is nullish (or just `"/"` / blank) but a `path` is
85
+ * provided, the URL is built against `http://localhost`, which is
86
+ * convenient for tests and for callers that resolve the host later.
87
+ *
88
+ * @example
89
+ * parseUrl("example.com"); // URL { https://example.com/ }
90
+ * parseUrl("http://example.com/path"); // URL { http://example.com/path }
91
+ * parseUrl({ url: "https://api.example" }); // URL { https://api.example/ }
92
+ * parseUrl("example.com", "/api", "v2", "items"); // URL { https://example.com/api/v2/items }
93
+ * parseUrl("example.com", ["api", "v2"]); // URL { https://example.com/api/v2 }
94
+ * parseUrl(null, "/api/x"); // URL { http://localhost/api/x }
95
+ * parseUrl(""); // null
96
+ * parseUrl(null); // null
97
+ */
98
+ export declare function parseUrl(input: UrlLike | null | undefined, ...path: UrlSegmentLike[]): URL | null;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Browser-safe networking helpers: URL parsing and path joining that
3
+ * gracefully handle partial inputs. No node-only imports, so this
4
+ * module is the canonical home for anything URL-shaped that also has
5
+ * to run in a Vite / Webpack / esbuild client bundle.
6
+ *
7
+ * Public API: {@link joinUrl}, {@link parseUrl}. The
8
+ * single-leading/trailing-slash stripper `stripBoundarySlashes` is
9
+ * private to this module.
10
+ *
11
+ * The server-side `./net.ts` re-exports everything here verbatim and
12
+ * tacks on its own node-only helpers (e.g. `getRandomPort`), so the
13
+ * `netUtils` namespace looks identical from both entry points.
14
+ */
15
+ // ────────────────────────────────────────────────────────────────
16
+ // URL helpers
17
+ // ────────────────────────────────────────────────────────────────
18
+ /**
19
+ * Join URL path segments with `/`, with three pragmatic conveniences
20
+ * for building URLs piecemeal:
21
+ *
22
+ * 1. Nullish or blank segments are dropped, so callers don't have
23
+ * to guard around optional path components.
24
+ * 2. Each string segment has any leading or trailing `/` stripped
25
+ * before joining, so `"/api"` + `"v2/"` round-trips to `/api/v2`
26
+ * regardless of how the caller terminated each part.
27
+ * 3. Array segments recurse, with their joined form inlined into
28
+ * the outer result.
29
+ *
30
+ * The result is prefixed with `/` to make it path-absolute, except
31
+ * when any segment carries an explicit `://` scheme (e.g.
32
+ * `"https://x.com"`), in which case the scheme is preserved verbatim.
33
+ *
34
+ * Returns `""` when every input was nullish or blank.
35
+ *
36
+ * @example
37
+ * joinUrl("a", "b"); // "/a/b"
38
+ * joinUrl("/api/", "/v2/", "x"); // "/api/v2/x"
39
+ * joinUrl(["a", "b"], "c"); // "/a/b/c"
40
+ * joinUrl("https://ex.com", "/api/x"); // "https://ex.com/api/x"
41
+ * joinUrl(null, "", "x", undefined); // "/x"
42
+ * joinUrl(); // ""
43
+ * joinUrl(null); // ""
44
+ */
45
+ export function joinUrl(...urlSegments) {
46
+ const parts = [];
47
+ for (const segment of urlSegments) {
48
+ if (segment == null)
49
+ continue;
50
+ if (Array.isArray(segment)) {
51
+ // Recurse, then strip the inner result's leading `/` so the
52
+ // outer path-absolute prepend doesn't double up to `//a/b/c`.
53
+ const inner = joinUrl(...segment);
54
+ if (inner)
55
+ parts.push(stripBoundarySlashes(inner));
56
+ continue;
57
+ }
58
+ // `Array.isArray` narrows away the UrlSegmentArray branch but
59
+ // leaves TS believing `segment` could still be a `string` only,
60
+ // which it now is - the type cast is just to silence a known
61
+ // TS limitation around recursive interface narrowing.
62
+ const trimmed = segment.trim();
63
+ if (!trimmed)
64
+ continue;
65
+ parts.push(stripBoundarySlashes(trimmed));
66
+ }
67
+ const joined = parts.filter(Boolean).join("/");
68
+ if (!joined)
69
+ return "";
70
+ return joined.includes("://") ? joined : "/" + joined;
71
+ }
72
+ /**
73
+ * Coerce a {@link UrlLike} input into a parsed `URL`, returning `null`
74
+ * for anything that cannot be coerced (null/undefined, empty string,
75
+ * malformed URL, an object whose `url` field doesn't parse).
76
+ *
77
+ * Mirrors WHATWG `URL.parse(...)` semantics: parse on success, `null`
78
+ * on failure - never throws.
79
+ *
80
+ * Bare hostnames are upgraded to `https://` before parsing, so callers
81
+ * can hand in user-provided values like `"workspace.cloud.databricks.com"`
82
+ * without an explicit scheme.
83
+ *
84
+ * Optional trailing `path` arguments are appended via {@link joinUrl}.
85
+ * When `input` is nullish (or just `"/"` / blank) but a `path` is
86
+ * provided, the URL is built against `http://localhost`, which is
87
+ * convenient for tests and for callers that resolve the host later.
88
+ *
89
+ * @example
90
+ * parseUrl("example.com"); // URL { https://example.com/ }
91
+ * parseUrl("http://example.com/path"); // URL { http://example.com/path }
92
+ * parseUrl({ url: "https://api.example" }); // URL { https://api.example/ }
93
+ * parseUrl("example.com", "/api", "v2", "items"); // URL { https://example.com/api/v2/items }
94
+ * parseUrl("example.com", ["api", "v2"]); // URL { https://example.com/api/v2 }
95
+ * parseUrl(null, "/api/x"); // URL { http://localhost/api/x }
96
+ * parseUrl(""); // null
97
+ * parseUrl(null); // null
98
+ */
99
+ export function parseUrl(input, ...path) {
100
+ if (typeof input === "string") {
101
+ input = input.trim();
102
+ const match = input.match(/^([a-z][a-z0-9+.-]*):\/\/(.*)$/i);
103
+ const rest = match?.[2] ?? input;
104
+ if (!rest || rest === "/")
105
+ return parseUrl(null, ...path);
106
+ const scheme = match?.[1];
107
+ if (!scheme)
108
+ input = `https://${input}`;
109
+ }
110
+ const joinedPath = joinUrl(...path);
111
+ if (input == null) {
112
+ if (joinedPath)
113
+ return parseUrl("http://localhost", joinedPath);
114
+ return null;
115
+ }
116
+ if (input instanceof URL) {
117
+ if (joinedPath)
118
+ return parseUrl(input.toString(), joinedPath);
119
+ return input;
120
+ }
121
+ if (typeof input === "string") {
122
+ const candidate = joinUrl(input, joinedPath);
123
+ if (candidate) {
124
+ try {
125
+ return new URL(candidate);
126
+ }
127
+ catch {
128
+ // Fall through to `null`.
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ return parseUrl(input.url, joinedPath);
134
+ }
135
+ // ────────────────────────────────────────────────────────────────
136
+ // Private helpers
137
+ // ────────────────────────────────────────────────────────────────
138
+ /** Strip a single leading `/` and a single trailing `/`, if present. */
139
+ function stripBoundarySlashes(s) {
140
+ let out = s;
141
+ if (out.startsWith("/"))
142
+ out = out.slice(1);
143
+ if (out.endsWith("/"))
144
+ out = out.slice(0, -1);
145
+ return out;
146
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Server-side networking helpers. Re-exports every browser-safe URL
3
+ * helper from {@link ./net.browser.ts} verbatim and adds node-only
4
+ * additions (currently {@link getRandomPort}), so the `netUtils`
5
+ * namespace exposes the same URL surface from either entry point and
6
+ * picks up node-only helpers automatically on the server.
7
+ */
8
+ export * from "./net.browser.js";
9
+ /**
10
+ * Bind a transient TCP listener on port `0`, read the OS-assigned
11
+ * port, close the listener, and resolve with the port. Used to grab
12
+ * a free local port for tests, devloops, and child processes.
13
+ */
14
+ export declare function getRandomPort(): Promise<number>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Server-side networking helpers. Re-exports every browser-safe URL
3
+ * helper from {@link ./net.browser.ts} verbatim and adds node-only
4
+ * additions (currently {@link getRandomPort}), so the `netUtils`
5
+ * namespace exposes the same URL surface from either entry point and
6
+ * picks up node-only helpers automatically on the server.
7
+ */
8
+ import net from "node:net";
9
+ export * from "./net.browser.js";
10
+ /**
11
+ * Bind a transient TCP listener on port `0`, read the OS-assigned
12
+ * port, close the listener, and resolve with the port. Used to grab
13
+ * a free local port for tests, devloops, and child processes.
14
+ */
15
+ export async function getRandomPort() {
16
+ return await new Promise((resolve, reject) => {
17
+ const server = net.createServer();
18
+ server.listen(0, () => {
19
+ const address = server.address();
20
+ if (!address || typeof address === "string") {
21
+ reject(new Error("Failed to get port"));
22
+ return;
23
+ }
24
+ const port = address.port;
25
+ server.close(() => resolve(port));
26
+ });
27
+ server.on("error", reject);
28
+ });
29
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Project introspection helpers shared across AppKit plugins.
3
+ *
4
+ * Resolve a human-friendly project name and parse git remote URLs into
5
+ * repo names. Exposed as `projectUtils.*` from the shared barrel so
6
+ * naming inside this module drops the redundant `project` prefix:
7
+ * `projectUtils.name()` instead of `projectName()`, etc.
8
+ *
9
+ * **Server-only.** Imports `node:fs`, `node:path`, `node:child_process`,
10
+ * and `node:util` at module load. Browser bundles must use
11
+ * `@dbx-tools/shared`'s `index.client.ts` entry point, which
12
+ * skips this module entirely.
13
+ */
14
+ export interface NameOptions {
15
+ /** Directory to start from. Defaults to `process.cwd()`. */
16
+ cwd?: string;
17
+ }
18
+ /**
19
+ * Resolve a human-friendly project name for the repo rooted at `cwd`.
20
+ *
21
+ * Order:
22
+ * 1. `name` from the root `package.json` (via `npm pkg get name` when available,
23
+ * otherwise read the file after locating the root).
24
+ * 2. Repository name from `git remote get-url origin`.
25
+ * 3. Basename of the project root directory.
26
+ */
27
+ export declare function name(options?: NameOptions): Promise<string>;
28
+ /**
29
+ * Parse a git remote URL (`https://...`, `git@host:owner/repo.git`, etc.)
30
+ * and return the repo segment, stripping any `.git` suffix. Returns
31
+ * `undefined` for empty or unparsable input.
32
+ */
33
+ export declare function parseGitRemote(url: string): string | undefined;